/* WITNESS · active-menu-window (W-001)
 *
 * What the journey PROMISES (from its own step list):
 *   3  a FRESH scan out of hours shows a menus-closed state and ZERO item cards
 *   7  a live session out of window resolves ONLY the login menu (breakfast), NOT dinner
 *
 * The canary asserts this with a FROZEN clock. I use the real one, which is the stronger
 * test, because a diner's phone has a real clock.
 *
 * How the fixture is shaped (read off its own payload):
 *   06:00-10:00  breakfast, 14 items
 *   17:00-23:00  dinner,     1 item  ("Canary Dinner Special", $22.99)
 *
 * To stand outside both windows I set the CONTEXT TIMEZONE, and I label that plainly as a
 * probe rather than dressing it up as an ordinary visit. It is not a trick: the app decides
 * open/closed from the device's local time, so a diner whose phone is in another timezone -
 * a traveller, or anyone whose phone is simply wrong - gets exactly this. That the gate is
 * driven by a client-supplied clock is itself worth someone's attention.
 *
 * The falsification I owe this claim: if the CLOSED state renders and no dinner item appears,
 * my false-green is wrong and I retract it. Step 1 is the control that makes step 2 mean
 * something - without proving the API says "closed" first, a rendered menu proves nothing.
 */
import { test, devices } from "@playwright/test";
import { journal, look, closeRole, PATRON, DWELL } from "../lib-witness.mjs";
import fs from "node:fs";

const TABLE = 9911;
const TZ    = "Asia/Dhaka";      // UTC+6: ~12:xx local, between the two windows

test("active-menu-window", async ({ browser }) => {
  // The fixture is keyed "canary-hours", not by the journey slug. My first run read the slug,
  // got undefined, and the control step stopped the walk instead of reporting a finding off a
  // page that never loaded a menu. That is the guard working, and it is why step 02 exists.
  const fx  = JSON.parse(fs.readFileSync("/workspace/uat/runs/wave0/fixtures.json", "utf8"));
  const rid = fx["canary-hours"];
  if (!rid) throw new Error("no canary-hours fixture id");
  const jr = journal("active-menu-window");
  const d  = devices["iPhone 14 Pro"];

  const ctx = await browser.newContext({
    ...d, timezoneId: TZ,
    recordVideo: { dir: `${jr.dir}/patron`,
                   size: { width: d.viewport.width * 2, height: d.viewport.height * 2 } },
  });
  jr.device("patron", "iPhone 14 Pro", d, browser);
  jr.target({ app: "patron", url: PATRON, restaurantId: rid, table: TABLE, timezoneId: TZ });
  jr.note(`_This fixture serves breakfast 06:00-10:00 and dinner 17:00-23:00. I arrive with `
        + `the phone's timezone set to **${TZ}**, which puts local time between the two - `
        + `closed. A real diner gets this by travelling, or by having a phone set wrong. The `
        + `journey promises a closed state and zero dishes._`);

  const page = await ctx.newPage();
  const seen = [];
  page.on("response", async (r) => {                    // watch what the server actually says
    if (!/menu/i.test(r.url())) return;
    try {
      const j = await r.json();
      if (j && ("menus_closed" in j || Array.isArray(j.items)))
        seen.push({ closed: j.menus_closed, items: (j.items || []).length,
                    url: r.url().split("?")[0] });
    } catch { /* not json */ }
  });

  try {
    let localTime;
    await jr.step("PATRON", "my phone agrees the restaurant is shut right now", async (id) => {
      await page.goto(`${PATRON}/menu?id=${rid}&table=${TABLE}`, { waitUntil: "domcontentloaded" });
      localTime = await page.evaluate(() => new Date().toLocaleTimeString("en-GB"));
      await look(page, jr, "patron", id, "arrival");
      return `My phone reads ${localTime} local (${TZ}) - after breakfast, before dinner.`;
    });

    await jr.step("PATRON", "the very first thing the server tells my phone is that it is shut",
      async (id) => {
        await page.waitForTimeout(DWELL);
        const first = seen[0];
        if (!first) throw new Error("I never saw a menu response at all, so I cannot say.");
        if (first.closed !== true)
          throw new Error(`The server's FIRST answer was menus_closed=${first.closed} with `
                        + `${first.items} items - so it does not agree it is shut, and the rest `
                        + `of this walk would prove nothing. My earlier finding is wrong.`);
        return `Its first answer is menus_closed=true with ${first.items} items. The gate `
             + `works at the server. Everything after this is about what my phone then SHOWS me.`;
      });

    await jr.step("PATRON", "so the screen tells me they are closed, and offers me no dishes",
      async (id) => {
        await page.waitForTimeout(DWELL * 2);
        const t = await look(page, jr, "patron", id, "what I am actually shown");
        const tiles  = await page.locator('[data-testid^="m2-tile-"]').count();
        const closed = /closed|not serving|come back|unavailable|opens at/i.test(t);
        const dinner = /Dinner Special|22\.99/i.test(t);
        const calls  = seen.map(s => `closed=${s.closed}/items=${s.items}`).join(" then ");

        if (closed && !tiles)
          return `It does. I am told they are closed and there is nothing to order. `
               + `Server calls: ${calls}. My earlier finding was WRONG and I retract it.`;

        throw new Error(
          `It does not. I am shown ${tiles} dishes and no closed message`
          + (dinner ? `, including "Canary Dinner Special" - a 17:00-23:00 dish, at `
                    + `${localTime} local` : "")
          + `. The server was asked the same question twice: ${calls} - the only thing that `
          + `changed between them is that the app minted a table session for me. `
          + `Screen reads: ${t.slice(0, 200)}`);
      });

    jr.finish("WALKED", "The closed state held and I retract my earlier finding.");
  } catch (e) {
    const calls = seen.map(s => `closed=${s.closed}/items=${s.items}`).join(" then ");
    jr.finish("STOPPED",
      `${String(e.message || e).split("\n")[0]} `
      + `\n\nThe server's own answers across this walk: ${calls}. The gate is correct at the `
      + `API on a fresh scan; what a person is SHOWN is the thing that diverges.`);
    throw e;
  } finally {
    await closeRole(ctx, jr, "patron");
  }
});
