/**
 * Shared staking helpers for holdout scenarios.
 *
 * stakeKrk   — drives the stake page UI (navigate → fill → submit → wait for Ponder).
 * unstakeKrk — expands the active position collapse and clicks Unstake.
 *
 * Both helpers handle the password gate on first visit automatically.
 */
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { navigateSPA } from '../../../tests/setup/navigate';
import { getStackConfig } from '../../../tests/setup/stack';

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

/**
 * Navigate to /app/stake, handling the auth guard's password gate if triggered.
 * The guard redirects to /login when localStorage 'authentificated' is absent.
 * After a successful login, router.push('/') causes '/' → '/app/stake' redirect so
 * we end up on the stake page without a second navigateSPA call.
 */
async function navigateToStakePage(page: Page): Promise<void> {
  await navigateSPA(page, '/app/stake');

  // Race between the stake form and the login page to determine which route mounted.
  // Point-in-time isVisible() is unreliable here: CSS transitions or async component
  // setup can leave the element in the DOM but not yet "visible" right after networkidle.
  const isLoginPage = await page
    .getByLabel('Password')
    .waitFor({ state: 'visible', timeout: 3_000 })
    .then(() => true)
    .catch(() => false);

  if (isLoginPage) {
    console.log('[stake] Password prompt detected, entering lobsterDao...');
    await page.getByLabel('Password').fill('lobsterDao');
    await page.getByRole('button', { name: 'Login' }).click();
    // router.push('/') in LoginView → '/' redirects to '/app/stake' → stake page loads.
    // waitForLoadState('networkidle') is avoided here for the same reason as navigate.ts:
    // persistent WebSocket connections prevent the network from ever going idle.
    // Instead, wait for the URL to settle on the stake page as the readiness signal.
    await page.waitForURL('**/app/stake**', { timeout: 20_000 });
    console.log('[stake] Authenticated, stake page loading');
  }
}

/**
 * Poll the Ponder GraphQL API until at least one active position exists for owner.
 * Throws if no active position appears within timeoutMs.
 *
 * owner must be lowercase: the e2e tests and this helper both use address.toLowerCase()
 * to match Ponder's storage format (verified against 01-acquire-and-stake.spec.ts).
 */
async function waitForActivePosition(graphqlUrl: string, owner: string, timeoutMs = 30_000): Promise<void> {
  const query = `
    query PositionsByOwner($owner: String!) {
      positionss(where: { owner: $owner }, limit: 5) {
        items {
          id
          status
        }
      }
    }
  `;

  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const resp = await fetch(graphqlUrl, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ query, variables: { owner } }),
    });
    const payload = (await resp.json()) as {
      data?: { positionss?: { items?: Array<{ id: string; status: string }> } };
    };
    const items = payload?.data?.positionss?.items ?? [];
    const active = items.filter(p => p.status === 'Active');
    if (active.length > 0) {
      console.log(`[stake] Ponder indexed ${active.length} active position(s)`);
      return;
    }
    console.log('[stake] Waiting for Ponder to index staking position...');
    // eslint-disable-next-line no-restricted-syntax -- Polling with timeout: Ponder GraphQL is HTTP-only (no push). See AGENTS.md #Engineering Principles.
    await new Promise(r => setTimeout(r, 2_000));
  }
  throw new Error(`No active staking position found in Ponder GraphQL within ${timeoutMs}ms`);
}

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

/**
 * Navigate to the stake page, fill the amount, select tax rate, and submit.
 * Wallet must already be connected. App must be password-unlocked.
 *
 * @param page - Playwright page with injected wallet
 * @param amount - KRK amount to stake (as string, e.g. '1000')
 * @param taxRateIndex - Tax rate option index (0-based). Use the highest available for max tax.
 */
export async function stakeKrk(page: Page, amount: string, taxRateIndex: number): Promise<void> {
  console.log(`[stake] Staking ${amount} KRK at tax rate index ${taxRateIndex}...`);

  await navigateToStakePage(page);

  // The Token Amount slider is the readiness signal used in the e2e test.
  const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' });
  await expect(tokenAmountSlider).toBeVisible({ timeout: 15_000 });
  console.log('[stake] Stake form loaded');

  // Fill staking amount.
  const stakeAmountInput = page.getByLabel('Staking Amount');
  await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 });
  await stakeAmountInput.fill(amount);
  console.log(`[stake] Filled staking amount: ${amount}`);

  // Select tax rate.
  const taxSelect = page.getByRole('combobox', { name: 'Tax' });
  await taxSelect.selectOption({ value: String(taxRateIndex) });
  console.log(`[stake] Tax rate index ${taxRateIndex} selected`);

  // Anchored regex avoids matching the "Unstake" buttons on any expanded position cards.
  const stakeButton = page.getByRole('main').getByRole('button', { name: /^(Snatch and )?Stake$/i });
  await expect(stakeButton).toBeVisible({ timeout: 5_000 });
  console.log('[stake] Clicking Stake button...');
  await stakeButton.click();

  // Wait for transaction: button cycles "Stake" → "Sign Transaction"/"Waiting" → "Stake".
  // The intermediate states may be missed if the tx completes instantly (Anvil automine).
  try {
    await page
      .getByRole('button', { name: /Sign Transaction|Waiting/i })
      .waitFor({ state: 'visible', timeout: 5_000 });
    console.log('[stake] Transaction initiated, waiting for completion...');
    await page
      .getByRole('main')
      .getByRole('button', { name: /^(Snatch and )?Stake$/i })
      .waitFor({ state: 'visible', timeout: 60_000 });
    console.log('[stake] Stake transaction completed');
  } catch {
    console.log('[stake] Transaction state not observed (may have completed instantly)');
  }

  // Resolve the connected account address to scope the Ponder query to this wallet.
  // Guard against an empty accounts array (locked or disconnected wallet).
  const accountAddress = await page.evaluate(async () => {
    const ethereum = (window as unknown as { ethereum: { request: (req: { method: string }) => Promise<string[]> } }).ethereum;
    const accounts = await ethereum.request({ method: 'eth_accounts' });
    if (!accounts || accounts.length === 0) {
      throw new Error('[stake] eth_accounts returned empty array — wallet may be locked');
    }
    return accounts[0].toLowerCase();
  });

  // Poll Ponder GraphQL until the new position is indexed (indexing latency up to 30s).
  const { graphqlUrl } = getStackConfig();
  await waitForActivePosition(graphqlUrl, accountAddress);
  console.log('[stake] ✅ Staking complete');
}

/**
 * Navigate to an active staking position and click Unstake.
 * Assumes the wallet has exactly one active position (simplest case).
 *
 * @param page - Playwright page with injected wallet
 */
export async function unstakeKrk(page: Page): Promise<void> {
  console.log('[stake] Unstaking active position...');

  await navigateToStakePage(page);

  // Wait for the active position collapse to appear.
  const activeCollapse = page.locator('.f-collapse-active').first();
  await expect(activeCollapse).toBeVisible({ timeout: 15_000 });
  console.log('[stake] Active position found');

  // FCollapse starts collapsed (isShow = false). Click the toggle icon to expand.
  // The icon carries class .toggle-collapse; clicking it fires openClose() in FCollapse.
  const toggleIcon = activeCollapse.locator('.toggle-collapse').first();
  await toggleIcon.click();
  console.log('[stake] Expanded position collapse');

  // The Unstake button appears in the collapsed body when unstake.state === 'Unstakeable'.
  const unstakeButton = activeCollapse.getByRole('button', { name: /^unstake$/i });
  await expect(unstakeButton).toBeVisible({ timeout: 10_000 });
  console.log('[stake] Clicking Unstake...');
  await unstakeButton.click();

  // Observe the sign/waiting state if visible (may be missed on fast Anvil automine).
  try {
    await activeCollapse
      .getByRole('button', { name: /Sign Transaction|Waiting/i })
      .waitFor({ state: 'visible', timeout: 5_000 });
    console.log('[stake] Unstake transaction initiated');
  } catch {
    console.log('[stake] Transaction sign/waiting state not observed (may have completed instantly)');
  }

  // Always wait for the collapse to detach — this verifies the Unstake click actually
  // worked and the position was removed, regardless of whether the transient states
  // were observed above.
  // TODO: add a Ponder waitForPositionGone poll here for holdout scenarios that assert
  //       off-chain state (e.g. status === 'Closed') immediately after unstaking.
  await activeCollapse.waitFor({ state: 'detached', timeout: 60_000 });
  console.log('[stake] Position removed from UI');

  console.log('[stake] ✅ Unstaking complete');
}
