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

/**
 * Guards the specific contract this rewrite exists to enforce
 * (PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §5.2): the amount actually charged
 * must be computed server-side, never sent by the client. Before this
 * rewrite, POST /cart carried a client-computed `amount` the backend was
 * asked to trust.
 *
 * cart.component.ts has no unit spec (no src/app/pages/cart/*.spec.ts
 * exists), so this E2E test is the only coverage the checkout request shape
 * has. Scoped narrowly on purpose: cart state is seeded directly into
 * localStorage and the customer session is faked via cookie + intercepted
 * session-check, rather than driving a full add-to-cart-then-login UI
 * journey - that journey is real product surface worth its own test, but
 * would make this test about navigation, not about what it exists to prove.
 */

const FAKE_SESSION_ID = 'e2e-fake-session';
const FAKE_ITEM = {
  categoryID: 1,
  itemID: 4242,
  name: 'E2E Test Item',
  photos: null,
  description: '',
  currency: 'RUB',
  price: 1000,
  discount: 0,
  rating: 0,
  callbacks: null,
  questions: null,
  quantity: 2,
};

test.describe('checkout request shape', () => {
  test.beforeEach(async ({ page, context }) => {
    await seedCart(page);
    await fakeCustomerSession(page, context);
    await mockFxQuoteEndpoint(page);
  });

  test('checkout session request carries offers and qty, never amount or price', async ({ page }) => {
    const checkoutRequest = interceptCheckoutSession(page);

    await page.goto('/cart');
    await acceptTermsAndCheckout(page);

    const body = await checkoutRequest;

    expect(body, 'must never send a client-computed amount').not.toHaveProperty('amount');
    expect(body, 'must never send a client-computed price').not.toHaveProperty('price');
    expect(Array.isArray(body.offers), 'must send an offers array').toBe(true);
    expect(body.offers[0]).toMatchObject({ offerId: String(FAKE_ITEM.itemID), qty: FAKE_ITEM.quantity });
  });

  test('payment intent request references the checkout session id, not a raw amount', async ({ page }) => {
    interceptCheckoutSession(page); // must resolve for the intent call to fire at all
    const intentRequest = interceptPaymentIntent(page);

    await page.goto('/cart');
    await acceptTermsAndCheckout(page);

    const body = await intentRequest;

    // Payment creation now goes through @marketplaces/payment
    // (MARKETPLACES_PAYMENT_GATEWAY -> POST {qrApiUrl}/api/v1/payments),
    // not api.service.ts's superseded createPaymentIntent - see
    // cart.component.ts's createPaymentIntent() comment.
    expect(body.checkoutSessionId, 'must reference the session created in step 1').toBe('chk_e2e_fixture');
    expect(body).not.toHaveProperty('amount');
    const metadata = body.metadata as Record<string, string> | undefined;
    expect(typeof metadata?.merchantReference).toBe('string');
    expect((metadata?.merchantReference ?? '').length).toBeGreaterThan(0);
  });
});

async function seedCart(page: Page): Promise<void> {
  await page.addInitScript(item => {
    window.localStorage.setItem('marketplace_cart', JSON.stringify([item]));
  }, FAKE_ITEM);
}

async function fakeCustomerSession(page: Page, context: import('@playwright/test').BrowserContext): Promise<void> {
  // Root-caused and fixed 2026-08-21 (see api-headers.interceptor.ts):
  // apiHeadersInterceptor injected AuthService to attach a WebSessionID
  // header, but AuthService's own constructor makes the exact
  // GET /users/sessions/:id call this interceptor runs on, which threw
  // NG0200 (circular dependency) mid-construction on every page load -
  // swallowed silently, read as "session invalid," cookie cleared
  // immediately. The { url } cookie form below is unrelated to that bug but
  // is still the more correct form, so it stays.
  await context.addCookies([
    {
      name: 'webSessionID',
      value: FAKE_SESSION_ID,
      url: 'http://localhost:4200',
    },
  ]);

  // Matches TelegramSessionApiService.normalizeWebSession's expected shape.
  await page.route('**/users/sessions/**', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        sessionId: FAKE_SESSION_ID,
        status: 'active',
        username: 'e2e_user',
        userId: 1,
      }),
    });
  });
}

async function mockFxQuoteEndpoint(page: Page): Promise<void> {
  await page.route('**/api/v2/pricing/fx-quote**', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        quoteId: 'fxq_e2e',
        base: 'RUB',
        quote: 'RUB',
        rate: 1,
        source: 'e2e-fixture',
        observedAt: new Date().toISOString(),
        expiresAt: new Date(Date.now() + 300_000).toISOString(),
      }),
    });
  });
}

function interceptCheckoutSession(page: Page): Promise<Record<string, unknown>> {
  return new Promise(resolve => {
    page.route('**/api/v2/storefront/checkout', (route: Route) => {
      const body = route.request().postDataJSON();
      resolve(body);
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          checkoutSessionId: 'chk_e2e_fixture',
          lines: [{
            offerId: String(FAKE_ITEM.itemID),
            qty: FAKE_ITEM.quantity,
            unitPrice: { amountMinor: FAKE_ITEM.price * 100, currency: 'RUB' },
            lineTotal: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
            priceSnapshotId: 'snap_e2e',
          }],
          subtotal: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
          discount: { amountMinor: 0, currency: 'RUB' },
          delivery: { amountMinor: 0, currency: 'RUB' },
          total: { amountMinor: FAKE_ITEM.price * FAKE_ITEM.quantity * 100, currency: 'RUB' },
          fxQuoteId: 'fxq_e2e',
          expiresAt: new Date(Date.now() + 300_000).toISOString(),
        }),
      });
    });
  });
}

function interceptPaymentIntent(page: Page): Promise<Record<string, unknown>> {
  return new Promise(resolve => {
    // @marketplaces/payment: apiUrl (qrApiUrl with its trailing /api
    // stripped, see app.config.ts) + default paymentsPath '/api/v1/payments'.
    page.route('**/api/v1/payments', (route: Route) => {
      const body = route.request().postDataJSON();
      resolve(body);
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          paymentId: 'qr_e2e_fixture',
          method: 'qr',
          status: 'pending',
          action: { type: 'qr', url: 'https://example.com/pay/e2e' },
        }),
      });
    });
  });
}

async function acceptTermsAndCheckout(page: Page): Promise<void> {
  await page.waitForLoadState('networkidle');

  // #terms-checkbox is a custom-styled input (zero-size native element, a
  // <label> renders the visible box) - .check() refuses on geometry even
  // with force:true, so toggle it via its label the way a real user would.
  const termsCheckbox = page.locator('#terms-checkbox');
  if (await termsCheckbox.count() > 0) {
    const label = page.locator('label[for="terms-checkbox"]');
    if (await label.count() > 0) {
      await label.click();
    } else {
      await termsCheckbox.dispatchEvent('click');
    }
  }

  const qrButton = page.getByRole('button', { name: /qr/i }).first();
  await qrButton.click();
}
