/* WITNESS · mosaic_v2-closed-resume   (ledger W-032)
   Canary promise: guards the Anant table 20 prod bug (2026-06-06) on the mosaic_v2 surface -
   a diner whose tab is BACKGROUNDED when the waiter closes the table misses the WS
   sessionEnded frame, and on resume re-renders the STALE closed menu instead of a
   ScanQR/ThankYou blocked screen.

   Previous witness run (2026-09-04, PARTIAL_GREEN, STR-1209 filed) stopped at step 3: no
   control anywhere in the owner app closed a table, and the only waiter login this container
   held ("No tables assigned") could not reach any canary restaurant's Tables tab.

   The 2026-09-05 credential pack gives Canary Mosaic its own waiter (HOUSE.waiter), and
   anant-close-table.spec.mjs already proved the real control is a SWIPE ("Swipe to Close
   Table" on the table's Bill tab) - not a text button, which is why an earlier /repro/ regex
   for "close table|end session" never matched. This re-drive uses the real control to try to
   turn the hinge this journey has never turned: a genuinely closed table, resumed cold.        */
import { test, expect } from "@playwright/test";
import { journal, look, arriveAsDiner, signInWaiter, DWELL, roleContext, closeRole,
         account, HOUSE, table, PATRON } from "../lib-witness.mjs";
import fs from "node:fs";

const RID = HOUSE.restaurantId;
const T   = table(10096);          // untouched by any other spec as of 2026-09-05

test("mosaic_v2-closed-resume", async ({ browser }) => {
  const jr = journal("mosaic_v2-closed-resume");
  jr.target({ app: "patron (mosaic_v2) + waiter", baseUrl: PATRON, restaurantId: RID, table: T,
              substitution: "none - Canary Mosaic's own waiter account, no surrogate this time" });
  jr.note("**The promise:** my phone goes in my pocket and loses signal. While it is away, "
        + "staff close my table. When I take it back out, the app should tell me the meal is "
        + "over - not silently carry on showing the menu I was browsing.");
  jr.note("_Previous run (2026-09-04) was PARTIAL_GREEN: no owner/waiter control could be found "
        + "to close a table at all. This run has a dedicated waiter account for this restaurant "
        + "and the real swipe-to-close control found on Anant table 10 - trying to get past the "
        + "hinge for the first time._");

  const cP = await roleContext(browser, jr, "patron");
  const cW = await roleContext(browser, jr, "waiter", "Desktop Chrome");
  const p = await cP.newPage(), w = await cW.newPage();

  let verdict = "INCOMPLETE", why = "", leftBehind = "nothing";
  const notes = {};

  try {
    /* 1 */
    await jr.step("PATRON", `I scan in at table ${T} and the menu loads (real WS join)`, async id => {
      const gates = await arriveAsDiner(p, RID, T, HOUSE.layout);
      await expect(p.locator('[data-testid^="m2-add-"]').first(),
                   "there should be dishes I can add").toBeVisible({ timeout: 30000 });
      const add = p.locator('[data-testid^="m2-add-"]').first();
      await add.click({ timeout: 25000 });
      await p.waitForTimeout(2500);
      const confirm = p.getByRole("button", { name: /add to (order|cart)|^add$|confirm/i }).first();
      if (await confirm.count()) { await confirm.click({ timeout: 15000 }).catch(() => {}); await p.waitForTimeout(2000); }
      const t = await look(p, jr, "patron", id, "menu on arrival, one dish in the cart");
      const n = await p.locator('[data-testid^="m2-add-"]').count();
      leftBehind = `an open session at table ${T} with one item in the cart`;
      return `gates: ${gates.join(", ") || "none"}; ${n} dishes visible; one added to cart. ${t.slice(0, 200)}`;
    });

    /* 2 */
    await jr.step("PATRON", "My phone goes in my pocket: screen off, signal gone", async id => {
      await p.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
      await cP.setOffline(true);
      await p.waitForTimeout(15000);
      notes.step2Note = "tab hidden + network cut for 15s, unattended";
      return "tab hidden and network severed for 15s - what a phone in a pocket looks like to the app";
    });

    /* 3 — the hinge this journey has never turned. */
    await jr.step("WAITER", `Staff sign in and close table ${T} for real, via the swipe control`, async id => {
      const acc = account(HOUSE.waiter);
      const landed = await signInWaiter(w, acc.email, acc.password);
      notes.waiterLandedAt = landed;
      await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      const card = w.getByTestId(`table-card-${T}`);
      await expect(card, `table ${T} should appear on the waiter's board`).toBeVisible({ timeout: 30000 });
      const before = (await card.innerText()).replace(/\s+/g, " ");
      notes.cardBeforeClose = before;
      if (/no guests/i.test(before))
        throw new Error(`I am sitting at table ${T} and the waiter's board already reads "no guests" `
                      + `before I even tried to close it - the session never registered. Card: ${before}`);
      await card.click({ timeout: 25000 });
      await w.waitForTimeout(DWELL);
      const billTab = w.getByTestId("table-detail-tab-bill");
      if (await billTab.count()) { await billTab.click({ timeout: 20000 }); await w.waitForTimeout(2000); }
      const t0 = await look(w, jr, "waiter", id + "a", "table detail, looking for a way to close");

      /* Find the TRACK, not the label (see anant-close-table.spec.mjs): the knob sits at the
         track's left edge, well left of where the label text renders. */
      const geo = await w.evaluate(() => {
        const el = [...document.querySelectorAll("*")]
          .find(e => e.children.length === 0 && /swipe to close/i.test(e.textContent || ""));
        if (!el) return null;
        let track = el;
        while (track && track.getBoundingClientRect().width < 250) track = track.parentElement;
        if (!track) return null;
        const r = track.getBoundingClientRect();
        return { x: r.x, y: r.y, w: r.width, h: r.height };
      });
      if (!geo) {
        notes.billScreenIds = await w.evaluate(() => [...document.querySelectorAll("[data-testid]")]
          .map(e => e.getAttribute("data-testid")).sort());
        throw new Error(`no "swipe to close" control on screen for table ${T}. Screen read: `
                      + `${t0.slice(0, 240)}. Controls present: ${notes.billScreenIds.slice(0, 30).join(", ")}`);
      }
      jr.note(`> **The track:** x ${Math.round(geo.x)}, y ${Math.round(geo.y)}, `
            + `${Math.round(geo.w)}×${Math.round(geo.h)}. The knob sits at its left edge.`);
      const y  = geo.y + geo.h / 2;
      const x0 = geo.x + Math.min(28, geo.h / 2);
      const x1 = geo.x + geo.w - 10;
      await w.mouse.move(x0, y);
      await w.mouse.down();
      for (let i = 1; i <= 20; i++) { await w.mouse.move(x0 + ((x1 - x0) * i) / 20, y); await w.waitForTimeout(40); }
      await w.mouse.up();
      await w.waitForTimeout(DWELL);
      const t1 = await look(w, jr, "waiter", id, "after the swipe-to-close drag");

      await w.reload({ waitUntil: "domcontentloaded" });
      await w.waitForTimeout(DWELL);
      await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(3000);
      const after = (await w.getByTestId(`table-card-${T}`).innerText()).replace(/\s+/g, " ");
      notes.cardAfterClose = after;
      if (!/no guests/i.test(after))
        throw new Error(`I dragged the swipe-to-close control and table ${T} still reads "${after}" `
                      + `on the reloaded board - the table did not actually close. Before the drag: `
                      + `${before}. After the drag, before reload: ${t1.slice(0, 200)}`);
      return `table ${T} closed for real: was "${before}", now reads "${after}" after reload.`;
    });

    /* 4 */
    await jr.step("PATRON", "I take my phone back out - signal restored, tab foregrounded, NO fresh navigation", async id => {
      await cP.setOffline(false);
      await p.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
      await p.bringToFront();
      await p.waitForTimeout(20000);          // give any reconnect/poll a real chance to land
      const t = await look(p, jr, "patron", id, "the instant I look at the phone again, no reload");
      notes.resumeScreen = t.slice(0, 500);
      return t.slice(0, 300);
    });

    /* 5 — the whole point of the journey. */
    await jr.step("PATRON", "My phone tells me the meal is over - it does NOT silently keep showing the menu", async id => {
      const t = await look(p, jr, "patron", id, "final screen after the closed-table resume");
      const dishesStillAddable = await p.locator('[data-testid^="m2-add-"]').count();
      const looksBlocked = /(thank you|see you|come again|scan|table.*closed|session.*ended|no longer available)/i.test(t);
      notes.finalScreen = t.slice(0, 600);
      notes.dishesStillAddable = dishesStillAddable;
      notes.looksBlocked = looksBlocked;
      if (dishesStillAddable > 0 && !looksBlocked)
        throw new Error(`BUG REPRODUCED: table ${T} was genuinely closed by staff while my tab was `
                      + `backgrounded and offline, and coming back my phone still shows the live menu `
                      + `with ${dishesStillAddable} addable dishes - not a closed/thank-you screen. `
                      + `Screen reads: ${t.slice(0, 400)}`);
      return `dishes still addable: ${dishesStillAddable}; screen reads a blocked/closed surface: `
           + `${looksBlocked}. Text: ${t.slice(0, 300)}`;
    });

    verdict = "HONEST_GREEN";
    why = `Walked all five named steps for the first time on this journey. Table ${T} was closed `
        + `for real via the waiter's own swipe-to-close control (canary-mosaic-waiter-dev@), and `
        + `resuming the patron tab - foregrounded + network restored, no fresh navigation - showed `
        + `a blocked/closed surface rather than the stale live menu. The 2026-09-04 blocker (no `
        + `owner/waiter control could close a table) is resolved by the 2026-09-05 credential pack; `
        + `the Anant-table-20-class regression this journey guards against was NOT reproduced.`;

  } catch (e) {
    why = String(e.message || e).split("\n")[0];
    if (/BUG REPRODUCED/.test(why)) verdict = "FALSE_GREEN";
    else verdict = "PARTIAL_GREEN";
  } finally {
    fs.writeFileSync(`${jr.dir}/NOTES.json`, JSON.stringify(notes, null, 1));
    jr.note(`> **What I left behind:** ${leftBehind}.`);
    jr.finish(verdict, why);
    await closeRole(cP, jr, "patron");
    await closeRole(cW, jr, "waiter");
  }
  expect(verdict, why).toBe("HONEST_GREEN");
});
