import { test, expect, type APIRequestContext, type Page } from "@playwright/test";
import { API_URL, SEAT_CLASS_LOCAL, STATIONS, resultsUrl, sampleDepartDate, staffToken, passengerToken } from "../../fixtures/data";

/**
 * Track B — backoffice config → portal propagation. A staff user changes config via the same API the
 * backoffice calls; the passenger portal is then observed. Each test restores what it changed so the
 * shared seeded DB stays consistent for other specs.
 */

/** The "starting from" fare the portal shows for the seeded ETB trip (captured from POST /search). */
async function portalCardFareMinor(page: Page): Promise<number> {
  const done = page.waitForResponse(
    (r) => r.url().includes("/search") && r.request().method() === "POST",
  );
  await page.goto(resultsUrl(), { waitUntil: "domcontentloaded" });
  const body = await (await done).json();
  return body?.data?.outbound?.[0]?.faresByClass?.[0]?.baseFareMinor;
}

/** The USD display fare the portal shows for a non-Ethiopian (Other) search. */
async function portalUsdFare(page: Page): Promise<number> {
  const done = page.waitForResponse(
    (r) => r.url().includes("/search") && r.request().method() === "POST",
  );
  await page.goto(resultsUrl({ nationality: "Other" }), { waitUntil: "domcontentloaded" });
  const body = await (await done).json();
  return body?.data?.outbound?.[0]?.faresByClass?.[0]?.displayAmountMinor;
}

function authHeader() {
  return { Authorization: `Bearer ${staffToken()}` };
}

/** Find a CurrencyExchangeRate row id by its currency pair. */
async function rateId(request: APIRequestContext, from: string, to: string): Promise<string> {
  const rows = (await (await request.get(`${API_URL}/currencies`, { headers: authHeader() })).json())?.data ?? [];
  const row = rows.find((r: any) => r.fromCurrency === from && r.toCurrency === to);
  if (!row) throw new Error(`no ${from}->${to} currency rate`);
  return row.id;
}

test("PB-2: a backoffice seat-class base-price change propagates LIVE to portal search", async ({
  page,
  request,
}) => {
  const before = await portalCardFareMinor(page);
  expect(before).toBeGreaterThan(0);

  // Staff doubles the base price via the API the backoffice tariff-rates form uses.
  const patched = await request.patch(`${API_URL}/seat-classes/${SEAT_CLASS_LOCAL}`, {
    headers: authHeader(),
    data: { basePrice: 600 }, // seed-core seeds 300
  });
  expect(patched.ok()).toBeTruthy();

  try {
    const after = await portalCardFareMinor(page);
    // No server-side config cache → the new price shows on the very next search.
    expect(after).toBe(before * 2);
  } finally {
    await request.patch(`${API_URL}/seat-classes/${SEAT_CLASS_LOCAL}`, {
      headers: authHeader(),
      data: { basePrice: 300 },
    });
  }
});

test("BC-11 ✅ a non-admin PASSENGER is forbidden from rewriting exchange rates (C-8)", async ({
  request,
}) => {
  // A global JwtGuard (SharedAuthModule) means anonymous requests get 401 — so this is NOT an
  // unauthenticated hole. The PUT/PATCH handlers must ALSO carry @PassengerAdmin (as DELETE does) so
  // a regular authenticated passenger cannot rewrite FX rates.
  const anon = await request.put(`${API_URL}/fare-engine/exchange-rates`, {
    data: { fromCurrency: "USD", toCurrency: "ETB", rate: 999 },
  });
  expect(anon.status()).toBe(401); // authentication IS required

  const asPassenger = await request.put(`${API_URL}/fare-engine/exchange-rates`, {
    headers: { Authorization: `Bearer ${passengerToken()}` },
    data: { fromCurrency: "USD", toCurrency: "ETB", rate: 999, source: "E2E" },
  });
  expect(asPassenger.status()).toBe(403); // ✅ a regular passenger is forbidden (admin-only)

  // The PATCH-by-id handler must be equally protected.
  const patchAsPassenger = await request.patch(`${API_URL}/fare-engine/exchange-rates/${crypto.randomUUID()}`, {
    headers: { Authorization: `Bearer ${passengerToken()}` },
    data: { rate: 999 },
  });
  expect(patchAsPassenger.status()).toBe(403);

  // A staff admin can still write (proves the endpoint isn't simply broken).
  const asStaff = await request.put(`${API_URL}/fare-engine/exchange-rates`, {
    headers: { Authorization: `Bearer ${staffToken()}` },
    data: { fromCurrency: "USD", toCurrency: "ETB", rate: 100, source: "E2E" },
  });
  expect(asStaff.ok()).toBeTruthy();
});

test("BC-7 ✅ negative seat-class base price is rejected by the live API (M-1)", async ({
  request,
}) => {
  const res = await request.patch(`${API_URL}/seat-classes/${SEAT_CLASS_LOCAL}`, {
    headers: authHeader(),
    data: { basePrice: -500 }, // the API must now reject this (DTO @Min(0)), like the backoffice form
  });
  expect(res.status()).toBe(400); // ✅ negative fare rejected at the DTO layer

  // The stored fare is unchanged — a valid write still succeeds and returns the seeded 300.
  const restore = await request.patch(`${API_URL}/seat-classes/${SEAT_CLASS_LOCAL}`, {
    headers: authHeader(),
    data: { basePrice: 300 },
  });
  expect(restore.ok()).toBeTruthy();
  expect(((await restore.json())?.data ?? {}).baseFareMinor).toBe(300);
});

test("PB-2b: base-price field-name — /seat-classes accepts `basePrice` and it drives the fare", async ({
  request,
}) => {
  // Documents which field the live seat-class endpoint reads (basePrice → baseFareMinor). If a future
  // change renames it, this fails loudly (the tariff-rates vs /fleet/classes split, matrix §7 Q9).
  const res = await request.patch(`${API_URL}/seat-classes/${SEAT_CLASS_LOCAL}`, {
    headers: authHeader(),
    data: { basePrice: 300 }, // no-op value, just asserts the field is accepted
  });
  expect(res.ok()).toBeTruthy();
  const json = await res.json();
  const updated = json?.data ?? json;
  expect(updated.baseFareMinor ?? updated.basePrice).toBe(300);
});

test("PB-1: a backoffice FX-rate change propagates LIVE to portal USD pricing", async ({ page, request }) => {
  const id = await rateId(request, "USD", "ETB");
  const before = await portalUsdFare(page);
  expect(before).toBeGreaterThan(0);
  try {
    // Doubling the USD→ETB rate doubles the fare-engine's ETB fare and therefore the USD display fare.
    const patched = await request.patch(`${API_URL}/currencies/${id}`, { headers: authHeader(), data: { rate: 200 } });
    expect(patched.ok()).toBeTruthy();
    const after = await portalUsdFare(page);
    expect(after).toBe(before * 2); // search has no cache → the new rate shows immediately
  } finally {
    await request.patch(`${API_URL}/currencies/${id}`, { headers: authHeader(), data: { rate: 100 } });
  }
});

test("PB-4: a station added in the backoffice appears in the portal station list", async ({ request }) => {
  const code = `E2E${Date.now() % 100000}`;
  const created = await request.post(`${API_URL}/stations`, {
    headers: authHeader(),
    data: { code, name: `E2E Station ${code}`, city: "Testville", countryCode: "ET", sequence: 99, isOperational: true },
  });
  expect(created.ok()).toBeTruthy();
  const id = ((await created.json())?.data ?? {}).id;
  try {
    const rows = (await (await request.get(`${API_URL}/stations`)).json())?.data ?? [];
    expect(rows.some((s: any) => s.code === code)).toBe(true); // portal SearchWidget reads this list
  } finally {
    await request.delete(`${API_URL}/stations/${id}?cascade=true`, { headers: authHeader() }).catch(() => {});
  }
});

test("PB-10 ✅ deleting an FX rate makes pricing FAIL CLOSED, not a silent 1.0 fallback (M-5/H-2)", async ({ request }) => {
  const searchBody = {
    originStationId: STATIONS.A,
    destinationStationId: STATIONS.C,
    date: sampleDepartDate(),
    adultCount: 1,
    nationality: "OTHER", // USD — the fare engine needs the USD↔ETB rate to price
  };
  const usdFaresByClass = async (): Promise<any[]> => {
    const res = await request.post(`${API_URL}/search`, { headers: authHeader(), data: searchBody });
    expect(res.ok()).toBeTruthy();
    return (await res.json())?.data?.outbound?.[0]?.faresByClass ?? [];
  };
  // Control: with the USD→ETB rate present, the USD search returns a real priced fare.
  const before = await usdFaresByClass();
  expect(before.length).toBeGreaterThan(0);
  expect(before[0].displayAmountMinor).toBeGreaterThan(0);

  try {
    // Remove EVERY USD→ETB rate row (an earlier spec may have left a duplicate) so the pair is truly gone.
    const rows = (await (await request.get(`${API_URL}/currencies`, { headers: authHeader() })).json())?.data ?? [];
    for (const r of rows.filter((x: any) => x.fromCurrency === "USD" && x.toCurrency === "ETB")) {
      await request.delete(`${API_URL}/currencies/${r.id}`, { headers: authHeader() });
    }
    // With the USD→ETB pair gone, the fare engine must NOT silently substitute rate 1.0 (~100×
    // underpricing). It fails closed — no priced class is returned for the USD trip, instead of a
    // bogus parity-priced fare. (A booking attempt would likewise be rejected, not swallowed.)
    const after = await usdFaresByClass();
    expect(after.length).toBe(0); // ✅ no silent underpricing — no bogus fare offered
  } finally {
    // Restore the pair so later specs price correctly.
    await request.post(`${API_URL}/currencies`, {
      headers: authHeader(),
      data: { fromCurrency: "USD", toCurrency: "ETB", rate: 100 },
    });
  }
});
