import { Page, Route, expect, test } from '@playwright/test';

/**
 * Track Q Q4: currency switch must recalculate by FX quote. Explicitly,
 * "160 RUB" must not become "160 USD" - the number has to change, not just
 * the label next to it.
 *
 * Written before the checkout money-truth rewrite (frontend backlog F10-F16,
 * which deletes CurrencyRatesService's client-side float math and switches
 * checkout to a server-computed total per
 * docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5). This test exists so
 * that rewrite has something to break loudly if it silently stops converting.
 *
 * This session has no live backend to run against, so GET
 * /api/v2/pricing/fx-quote is intercepted with a response shaped exactly per
 * contract §3.1. That exercises the REAL code path - FxQuoteApiGateway,
 * CurrencyRatesService, the currencyConvert pipe - rather than switching the
 * whole app into mock mode, which would test a different (mock) gateway
 * instead of the one actually shipped.
 */

/** Rate relative to RUB, only what this test needs. */
const MOCK_RATE: Record<string, number> = { USD: 0.0108, EUR: 0.0092, AMD: 4.31 };

test.describe('currency switch', () => {
  test.beforeEach(async ({ page }) => {
    await mockFxQuoteEndpoint(page);
  });

  test('switching currency changes the displayed price value, not just its label', async ({ page }) => {
    await page.goto('/');
    await page.waitForLoadState('networkidle');

    const priceLocator = page.locator('.current-price, .original-price').first();
    await expect(priceLocator).toBeVisible({ timeout: 15_000 });

    const before = await readPrice(priceLocator);
    expect(before.value, 'a price must be a real positive number before switching').toBeGreaterThan(0);

    await switchCurrency(page, before.currency === 'USD' ? 'RUB' : 'USD');

    // The currency LABEL flips synchronously (a signal write), but the rate
    // itself arrives from the mocked network call asynchronously - polling
    // only the label races ahead of the actual conversion and passes before
    // the number has caught up. Poll the parsed numeric value instead, since
    // that is what this test exists to guard.
    await expect
      .poll(async () => (await readPrice(priceLocator)).value, {
        message: 'price value never diverged from the pre-switch amount',
      })
      .not.toBeCloseTo(before.value, 2);

    const after = await readPrice(priceLocator);

    expect(after.currency, 'the currency label must actually change').not.toBe(before.currency);
    // The literal regression this test exists to catch: a rate of 1 disguised
    // as a real conversion. RUB->USD or USD->RUB is never a 1:1 rate.
    expect(after.value, `${before.value} ${before.currency} must not equal ${after.value} ${after.currency}`).not.toBeCloseTo(before.value, 2);
  });

  test('an out-of-range rate must not silently pass as valid', async ({ page }) => {
    // Guards the specific bad-data class this suite exists to catch: a
    // conversion that returns something implausible (zero, negative, or
    // absurdly large) instead of erroring visibly.
    await page.goto('/');

    const priceLocator = page.locator('.current-price, .original-price').first();
    await expect(priceLocator).toBeVisible({ timeout: 15_000 });

    const { value } = await readPrice(priceLocator);

    expect(value).toBeGreaterThan(0);
    expect(value).toBeLessThan(100_000_000);
  });
});

async function mockFxQuoteEndpoint(page: Page): Promise<void> {
  await page.route('**/api/v2/pricing/fx-quote**', (route: Route) => {
    const url = new URL(route.request().url());
    const base = url.searchParams.get('base') ?? 'RUB';
    const quote = url.searchParams.get('quote') ?? 'USD';
    const rate = MOCK_RATE[quote] ?? 1;
    const now = new Date();

    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        quoteId: `fxq_e2e_${base}_${quote}_${now.getTime()}`,
        base,
        quote,
        rate,
        source: 'e2e-fixture',
        observedAt: now.toISOString(),
        expiresAt: new Date(now.getTime() + 5 * 60 * 1000).toISOString(),
      }),
    });
  });
}

async function switchCurrency(page: Page, targetCode: string): Promise<void> {
  // The page renders more than one language-selector instance (desktop/mobile
  // variants share the same markup) - scoping to the dropdown that actually
  // carries the "open" class avoids clicking an option in a hidden duplicate,
  // which is silently a no-op rather than a failure.
  const trigger = page.locator('.currency-button:visible').first();
  await trigger.click();

  const openDropdown = page.locator('.currency-dropdown.open').first();
  await expect(openDropdown).toBeVisible();

  await openDropdown.locator('.currency-option', { hasText: targetCode }).first().click();
}

async function readPrice(locator: import('@playwright/test').Locator): Promise<{ value: number; currency: string }> {
  const text = (await locator.textContent()) ?? '';
  // Matches "1 234.56 USD" / "1234.56 ₽" shapes the price templates render.
  const match = text.replace(/\s/g, '').match(/([\d.,]+)([A-Z]{3}|\D+)$/);
  if (!match) {
    throw new Error(`could not parse price text: "${text}"`);
  }
  const value = Number(match[1].replace(/,/g, ''));
  return { value, currency: match[2] };
}
