/**
 * Shared swap helpers for holdout scenarios.
 *
 * buyKrk    — drives the real get-krk page swap widget (UI path, requires #393 fix).
 * sellKrk   — drives the get-krk page sell widget UI (requires #456 sell tab).
 * sellAllKrk — submits approve + exactInputSingle directly via window.ethereum
 *              (no UI widget — the Uniswap router handles the on-chain leg).
 */
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { Interface } from 'ethers';
import { navigateSPA } from '../../../tests/setup/navigate';
import { rpcCall } from './rpc';

// Infrastructure addresses stable across Anvil forks of Base Sepolia
const SWAP_ROUTER = '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4';
const WETH = '0x4200000000000000000000000000000000000006';
const POOL_FEE = 10_000; // 1% tier used by the KRAIKEN pool

// ERC-20 Transfer event topic (keccak256("Transfer(address,address,uint256)"))
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';

const ERC20_ABI = ['function approve(address spender, uint256 amount) returns (bool)'];
const ROUTER_ABI = [
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
];

// ── Internal helpers ─────────────────────────────────────────────────────────

/** Read an ERC-20 balanceOf in the Node.js context via direct RPC. */
async function erc20BalanceOf(rpcUrl: string, tokenAddress: string, account: string): Promise<bigint> {
  const selector = '0x70a08231'; // balanceOf(address)
  const data = selector + account.slice(2).padStart(64, '0');
  return BigInt((await rpcCall(rpcUrl, 'eth_call', [{ to: tokenAddress, data }, 'latest'])) as string);
}

/**
 * Poll eth_getTransactionReceipt until the tx is mined or maxAttempts exceeded.
 * Anvil with automine resolves almost immediately; the loop guards against
 * instances configured with a block interval or high RPC latency.
 *
 * Throws if the transaction was mined but reverted (status 0x0) so callers
 * get a clear failure rather than a confusing downstream balance-assertion error.
 */
export async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): Promise<void> {
  for (let i = 0; i < maxAttempts; i++) {
    const receipt = (await rpcCall(rpcUrl, 'eth_getTransactionReceipt', [txHash])) as Record<
      string,
      unknown
    > | null;
    if (receipt !== null) {
      if (receipt.status === '0x0') {
        throw new Error(`Transaction ${txHash} reverted (status 0x0)`);
      }
      return; // status === '0x1' — success
    }
    // eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no event source for transaction receipt over HTTP RPC (eth_subscribe not available). See AGENTS.md #Engineering Principles.
    await new Promise(r => setTimeout(r, 500));
  }
  throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`);
}

// ── Public config type ───────────────────────────────────────────────────────

export interface BuyKrkOptions {
  /** Anvil JSON-RPC endpoint (used to query KRK balance after swap). */
  rpcUrl: string;
  /** Deployed KRAIKEN (KRK) ERC-20 contract address. */
  krkAddress: string;
  /** EOA address that will receive the KRK tokens. */
  accountAddress: string;
}

export interface SellConfig {
  /** Anvil JSON-RPC endpoint (used to wait for receipt and query token balances). */
  rpcUrl: string;
  /** Deployed KRAIKEN (KRK) ERC-20 contract address. */
  krkAddress: string;
  /** EOA address that holds the KRK tokens and will send the transactions. */
  accountAddress: string;
}

// ── Exported helpers ─────────────────────────────────────────────────────────

/**
 * Navigate to the get-krk page, fill the ETH amount, click Buy KRK, and wait for
 * the swap widget to return to its idle state ("Buy KRK" button re-enabled).
 *
 * Uses the real LocalSwapWidget UI path (requires the #393 fill() fix).
 * Wallet must already be connected before calling this.
 *
 * If opts is provided, creates an eth_newFilter for Transfer events to the account
 * and polls eth_getFilterLogs until the event arrives, ensuring the swap has been
 * mined on-chain before returning. Otherwise, just waits for the UI state transition
 * (caller is responsible for verification).
 *
 * @param screenshotPrefix - Optional prefix for screenshot filenames (e.g., 'walletA', 'walletB')
 */
export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions, screenshotPrefix = 'holdout'): Promise<void> {
  console.log(`[swap] Buying KRK with ${ethAmount} ETH via get-krk page...`);
  await navigateSPA(page, '/app/get-krk');
  await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });

  const swapInput = page.getByTestId('swap-amount-input');
  await expect(swapInput).toBeVisible({ timeout: 15_000 });

  await swapInput.fill(ethAmount);
  await page.waitForTimeout(500);

  const buyButton = page.getByTestId('swap-buy-button');
  await expect(buyButton).toBeVisible({ timeout: 5_000 });

  // Create Transfer event filter BEFORE the swap (if opts provided)
  let filterId: string | undefined;
  if (opts) {
    console.log('[swap] Creating Transfer event filter...');
    filterId = (await rpcCall(opts.rpcUrl, 'eth_newFilter', [
      {
        address: opts.krkAddress,
        topics: [
          TRANSFER_TOPIC,
          null, // any sender
          '0x' + opts.accountAddress.slice(2).padStart(64, '0'), // to our account
        ],
        fromBlock: 'latest',
      },
    ])) as string;
    console.log(`[swap] Filter created: ${filterId}`);
  }

  await page.screenshot({ path: `test-results/${screenshotPrefix}-before-buy.png` });
  console.log('[swap] Clicking Buy KRK...');
  await buyButton.click();

  // Button cycles: idle "Buy KRK" → "Submitting…" → idle "Buy KRK"
  try {
    await page.getByRole('button', { name: /Submitting/i }).waitFor({ state: 'visible', timeout: 5_000 });
    console.log('[swap] Swap in progress...');
    await expect(page.getByTestId('swap-buy-button')).toHaveText('Buy KRK', { timeout: 60_000 });
    console.log('[swap] Swap completed');
  } catch {
    // Swap completed before the Submitting state could be observed
    console.log('[swap] Button state not observed (swap may have completed instantly)');
  }

  // If opts provided, wait for the Transfer event to arrive
  if (opts && filterId) {
    console.log('[swap] Waiting for Transfer event...');
    const deadline = Date.now() + 15_000;
    let received = false;
    while (Date.now() < deadline) {
      const logs = (await rpcCall(opts.rpcUrl, 'eth_getFilterLogs', [filterId])) as unknown[];
      if (logs && logs.length > 0) {
        received = true;
        console.log(`[swap] Transfer event received (${logs.length} log(s))`);
        break;
      }
      // eslint-disable-next-line no-restricted-syntax -- Polling with timeout: eth_getFilterLogs is HTTP-only polling (not push). See AGENTS.md #Engineering Principles.
      await new Promise(r => setTimeout(r, 200));
    }
    // Clean up filter
    await rpcCall(opts.rpcUrl, 'eth_uninstallFilter', [filterId]).catch(() => {});
    if (!received) {
      throw new Error(`No KRK Transfer event received within 15s after buying with ${ethAmount} ETH`);
    }
  }

  await page.screenshot({ path: `test-results/${screenshotPrefix}-after-buy.png` });
}

/**
 * Navigate to the get-krk page, switch to Sell tab, fill KRK amount, click Sell.
 * Wallet must already be connected.
 *
 * Drives the real sell widget UI (requires the #456 sell tab).
 *
 * If config is provided, creates an eth_newFilter for WETH Transfer events to the
 * account before clicking Sell, polls eth_getFilterLogs until the event arrives
 * (confirming the swap is mined), then returns the WETH balance delta. Otherwise
 * returns 0n (caller is responsible for verification).
 *
 * @param page - Playwright page with injected wallet
 * @param amount - KRK amount to sell (as string). Use 'max' to click the Max button.
 * @param screenshotPrefix - Optional prefix for screenshot filenames
 * @param config - Optional config for on-chain WETH receipt confirmation
 * @returns WETH received (balance diff) or 0n if config is not provided
 */
export async function sellKrk(
  page: Page,
  amount: string,
  screenshotPrefix?: string,
  config?: Pick<SellConfig, 'rpcUrl' | 'accountAddress'>,
): Promise<bigint> {
  console.log(`[swap] Selling ${amount} KRK via get-krk page sell widget...`);
  await navigateSPA(page, '/app/get-krk');
  await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });

  const sellTab = page.getByTestId('swap-mode-sell');
  await expect(sellTab).toBeVisible({ timeout: 10_000 });
  await sellTab.click();

  const sellInput = page.getByTestId('swap-sell-amount-input');
  if (amount === 'max') {
    const maxButton = page.locator('.max-button');
    await expect(maxButton).toBeVisible({ timeout: 5_000 });
    await maxButton.click();
    // setMax() is async — wait for the composable to populate the input via loadKrkBalance()
    await expect(sellInput).not.toHaveValue('', { timeout: 10_000 });
    console.log('[swap] Clicked Max button');
  } else {
    await expect(sellInput).toBeVisible({ timeout: 5_000 });
    await sellInput.fill(amount);
    console.log(`[swap] Filled sell amount: ${amount}`);
  }

  const wethBefore = config ? await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress) : 0n;

  // Create WETH Transfer event filter BEFORE the sell (if config provided)
  let filterId: string | undefined;
  if (config) {
    filterId = (await rpcCall(config.rpcUrl, 'eth_newFilter', [
      {
        address: WETH,
        topics: [
          TRANSFER_TOPIC,
          null, // any sender (pool/router)
          '0x' + config.accountAddress.slice(2).padStart(64, '0'), // to our account
        ],
        fromBlock: 'latest',
      },
    ])) as string;
    console.log(`[swap] WETH Transfer filter created: ${filterId}`);
  }

  if (screenshotPrefix) {
    await page.screenshot({ path: `test-results/${screenshotPrefix}-before-sell.png` });
  }

  const sellButton = page.getByTestId('swap-sell-button');
  await expect(sellButton).toBeVisible({ timeout: 5_000 });
  console.log('[swap] Clicking Sell KRK...');
  await sellButton.click();

  // Button cycles: "Sell KRK" → "Approving…" / "Selling…" → "Sell KRK"
  try {
    await sellButton.filter({ hasText: /Approving…|Selling…/i }).waitFor({ state: 'visible', timeout: 5_000 });
    console.log('[swap] Sell in progress...');
  } catch {
    // Sell completed before the transient state could be observed
    console.log('[swap] Button state not observed (sell may have completed instantly)');
  }
  await expect(sellButton).toHaveText('Sell KRK', { timeout: 60_000 });
  console.log('[swap] Sell completed (UI idle)');

  // Wait for on-chain confirmation via WETH Transfer event
  if (config && filterId) {
    console.log('[swap] Waiting for WETH Transfer event...');
    const deadline = Date.now() + 15_000;
    let received = false;
    while (Date.now() < deadline) {
      const logs = (await rpcCall(config.rpcUrl, 'eth_getFilterLogs', [filterId])) as unknown[];
      if (logs && logs.length > 0) {
        received = true;
        console.log(`[swap] WETH Transfer event received (${logs.length} log(s))`);
        break;
      }
      // eslint-disable-next-line no-restricted-syntax -- Polling with timeout: eth_getFilterLogs is HTTP-only polling (not push). See AGENTS.md #Engineering Principles.
      await new Promise(r => setTimeout(r, 200));
    }
    await rpcCall(config.rpcUrl, 'eth_uninstallFilter', [filterId]).catch(() => {});
    if (!received) {
      throw new Error(`No WETH Transfer event received within 15s after selling ${amount} KRK`);
    }
  }

  if (screenshotPrefix) {
    await page.screenshot({ path: `test-results/${screenshotPrefix}-after-sell.png` });
  }

  if (!config) return 0n;

  const wethAfter = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);
  const wethReceived = wethAfter - wethBefore;
  if (wethReceived <= 0n) {
    console.warn('[swap] WARNING: WETH balance did not increase after sell — pool may have returned 0 output');
  } else {
    console.log(`[swap] Received ${wethReceived} WETH`);
  }
  return wethReceived;
}

/**
 * Query the current KRK balance, then approve the Uniswap router and swap
 * all KRK back to WETH via on-chain transactions submitted through the
 * injected window.ethereum provider.
 *
 * This is the "sovereign exit" path — it bypasses the UI swap widget and
 * sends transactions directly so the test is not gated on the sell-side UI.
 *
 * Throws if the WETH balance does not increase after the swap, which
 * indicates the pool returned 0 output (possible with amountOutMinimum: 0n on
 * a partially-drained pool).
 *
 * @returns The WETH delta (wethAfter - wethBefore) received from the swap.
 */
export async function sellAllKrk(page: Page, config: SellConfig): Promise<bigint> {
  const krkBalance = await erc20BalanceOf(config.rpcUrl, config.krkAddress, config.accountAddress);
  if (krkBalance === 0n) throw new Error('sellAllKrk: KRK balance is 0 — nothing to sell');

  console.log(`[swap] Selling ${krkBalance} KRK...`);

  const wethBefore = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);

  const erc20Iface = new Interface(ERC20_ABI);
  const routerIface = new Interface(ROUTER_ABI);

  const approveData = erc20Iface.encodeFunctionData('approve', [
    SWAP_ROUTER,
    BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
  ]);

  const swapData = routerIface.encodeFunctionData('exactInputSingle', [
    {
      tokenIn: config.krkAddress,
      tokenOut: WETH,
      fee: POOL_FEE,
      recipient: config.accountAddress,
      amountIn: krkBalance,
      amountOutMinimum: 0n,
      sqrtPriceLimitX96: 0n,
    },
  ]);

  // Step 1: approve KRK spend allowance to the Uniswap router
  console.log('[swap] Approving KRK to router...');
  const approveTxHash = await page.evaluate(
    ({ krkAddr, data, from }: { krkAddr: string; data: string; from: string }) =>
      (window.ethereum as any).request({
        method: 'eth_sendTransaction',
        params: [{ from, to: krkAddr, data, gas: '0x30000' }],
      }) as Promise<string>,
    { krkAddr: config.krkAddress, data: approveData, from: config.accountAddress },
  );
  await waitForReceipt(config.rpcUrl, approveTxHash);
  console.log('[swap] Approve mined');

  // Step 2: swap KRK → WETH via the Uniswap V3 router
  console.log('[swap] Swapping KRK → WETH (exit)...');
  const swapTxHash = await page.evaluate(
    ({ routerAddr, data, from }: { routerAddr: string; data: string; from: string }) =>
      (window.ethereum as any).request({
        method: 'eth_sendTransaction',
        params: [{ from, to: routerAddr, data, gas: '0x80000' }],
      }) as Promise<string>,
    { routerAddr: SWAP_ROUTER, data: swapData, from: config.accountAddress },
  );
  await waitForReceipt(config.rpcUrl, swapTxHash);
  console.log('[swap] Swap mined');

  const wethAfter = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);
  const wethReceived = wethAfter - wethBefore;
  if (wethReceived <= 0n) {
    throw new Error('sellAllKrk: swap returned 0 WETH — pool may be drained or price impact exceeded balance');
  }
  console.log(`[swap] Received ${wethReceived} WETH`);
  return wethReceived;
}
