/* WITNESS · cross-journey-ledger   (ledger W-004)
   The promise (canary desc): "Waiter serves ONE item + cancels ONE item of a 4-item order;
   the patron ledger shows Served/Cancelled/Ordered simultaneously in real-time and survives
   a hard reload (STR-681/682)."

   Previously DEFERRED D4 (1/13 steps): no staff account existed for this fixture. The
   2026-09-05 credential pack gives Canary Mosaic its own waiter, so diner and waiter now sit
   on ONE board and the whole journey is walkable.

   SCOPING: the waiter Orders list carries ~17 live cards from the scheduled fleet, and the
   last attempt died on a bare text match. A card names its own table ("T10092 ..."), so I
   scope to my table and refuse to act unless EXACTLY ONE card matches. A first attempt at a
   set-difference was my own bug (I snapshotted while on the Tables tab, where no order card
   renders) - recorded here so nobody reads that stop as a product finding. */
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(10092);          // confirmed "No guests" on the board at 19:12 UTC; throws if outside the block

const ids = (page, prefix) => page.evaluate(p =>
  [...new Set([...document.querySelectorAll(`[data-testid^="${p}"]`)]
    .map(e => e.getAttribute("data-testid").slice(p.length)))], prefix);
const allIds = page => page.evaluate(() =>
  [...new Set([...document.querySelectorAll("[data-testid]")].map(e => e.getAttribute("data-testid")))].sort());

test("cross-journey-ledger", async ({ browser }) => {
  const jr = journal("cross-journey-ledger");
  jr.target({ app: "patron (mosaic_v2) + waiter", baseUrl: PATRON, restaurantId: RID, table: T,
              substitution: "none - this IS the canary's own fixture, walked with its own waiter account" });
  jr.note("**The promise:** a waiter serves one dish and cancels another out of a four-dish order, "
        + "and the diner's phone shows all three states at once — served, cancelled, still coming — "
        + "and still does after the page is reloaded.");

  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 dishes = [];            // names of the four I ordered
  let before = [], myOrder = "", served = "", cancelled = "";
  const notes = {};

  try {
    /* 1 */
    await jr.step("PATRON", `I sit down at table ${T} and the menu comes up`, async id => {
      const gates = await arriveAsDiner(p, RID, T, HOUSE.layout);
      await expect(p.locator('[data-testid^="m2-add-"]').first(),
                   "there should be something I can order").toBeVisible({ timeout: 30000 });
      const t = await look(p, jr, "patron", id, "menu on arrival");
      const n = await p.locator('[data-testid^="m2-add-"]').count();
      return `gates: ${gates.join(", ") || "none"}; ${n} dishes I can add. ${t.slice(0, 200)}`;
    });

    /* 2 */
    await jr.step("PATRON", "I choose four dishes", async id => {
      for (let i = 0; i < 4; i++) {
        const add = p.locator('[data-testid^="m2-add-"]').nth(i);
        const iid = (await add.getAttribute("data-testid")).replace("m2-add-", "");
        const name = await p.locator(`[data-testid="m2-text-${iid}"]`).innerText()
                        .catch(() => `dish ${i + 1}`);
        await add.click({ timeout: 25000 });
        await p.waitForTimeout(2500);
        /* Some dishes ask a question before they go in the basket. Answer it as a person would. */
        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(2500); }
        dishes.push(name.replace(/\s+/g, " ").slice(0, 40).trim());
      }
      await p.waitForTimeout(DWELL);
      const t = await look(p, jr, "patron", id, "four dishes chosen");
      leftBehind = `an open basket at table ${T}`;
      return `I picked: ${dishes.join(" | ")}. ${t.slice(0, 200)}`;
    });

    /* 3 */
    await jr.step("WAITER", "The waiter can see somebody is at my table", async id => {
      const a = account(HOUSE.waiter);
      await signInWaiter(w, a.email, a.password);
      await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      const card = await w.getByTestId(`table-card-${T}`).innerText();
      await look(w, jr, "waiter", id, `tables board, table ${T}`);
      const txt = card.replace(/\s+/g, " ");
      notes.tableCardAtStep3 = txt;
      if (/no guests/i.test(txt))
        throw new Error(`I am sitting at table ${T} with four dishes in my basket and the waiter's `
                      + `board still says "No guests". Card reads: ${txt}`);
      return `table ${T} card reads "${txt}"`;
    });

    /* 4 */
    await jr.step("PATRON", "I place the order for all four", async id => {
      await p.locator('[data-testid="m2-dock-trigger"], [data-testid="m2-view-order"], [data-testid="m2-dock-cart"]')
             .first().click({ timeout: 25000 });
      await p.waitForTimeout(DWELL);
      await look(p, jr, "patron", id + "a", "my basket before ordering");
      await p.locator('[data-testid="order-now-btn"]').first().click({ timeout: 25000 });
      await p.waitForTimeout(DWELL + 5000);
      const t = await look(p, jr, "patron", id, "after placing the order");
      leftBehind = `a live session and a four-dish order at table ${T}`;
      return t.slice(0, 300);
    });

    /* 5 */
    await jr.step("WAITER", "My order shows up on the waiter's list", async id => {
      await w.reload({ waitUntil: "domcontentloaded" });
      await w.waitForTimeout(DWELL + 5000);
      await w.getByTestId("tab-orders").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      /* A card names its own table. Scope to mine, and refuse to act if that is not exactly one. */
      const mine = w.locator('[data-testid^="order-card-"]').filter({ hasText: new RegExp(`\\bT${T}\\b`) });
      const n = await mine.count();
      notes.cardsForMyTable = n;
      if (n !== 1)
        throw new Error(`I placed one order and the waiter's list shows ${n} cards for table ${T}. `
                      + `I cannot tell which is mine, so I will not act on any of them.`);
      myOrder = (await mine.first().getAttribute("data-testid")).replace("order-card-", "");
      await mine.first().scrollIntoViewIfNeeded().catch(() => {});
      const t = await look(w, jr, "waiter", id, "orders list with my order on it");
      const txt = (await mine.first().innerText()).replace(/\s+/g, " ");
      notes.myCardText = txt.slice(0, 500);
      notes.myCardIds = await w.evaluate(o => [...document.querySelectorAll(`[data-testid="order-card-${o}"] [data-testid]`)]
                                             .map(e => e.getAttribute("data-testid")), myOrder);
      jr.note(`> **My order card carries these controls:** \`${notes.myCardIds.join("\`, \`")}\``);
      const missing = dishes.filter(d => !txt.toLowerCase().includes(d.toLowerCase().split(" ").slice(0,2).join(" ")));
      if (missing.length) jr.note(`> Note: the waiter's card does not name ${missing.length} of the four I ordered.`);
      return `card ${myOrder.slice(0, 8)} reads: ${txt.slice(0, 300)} — page: ${t.slice(0, 120)}`;
    });

    /* 6 */
    await jr.soft("WAITER", "The waiter puts it into the till and starts the kitchen", async id => {
      const pos = w.getByTestId(`pos-entered-${myOrder}`);
      const act = w.getByTestId(`order-status-action-${myOrder}`);
      const hadPos = await pos.count() > 0, hadAct = await act.count() > 0;
      if (hadPos) { await pos.click({ timeout: 20000 }); await w.waitForTimeout(DWELL); }
      if (hadAct) { await act.click({ timeout: 20000 }).catch(() => {}); await w.waitForTimeout(DWELL); }
      const t = await look(w, jr, "waiter", id, "after till + kitchen start");
      const txt = (await w.getByTestId(`order-card-${myOrder}`).innerText()).replace(/\s+/g, " ");
      if (!hadPos && !hadAct)
        throw new Error("there is no till control and no kitchen-start control on my order card at all. "
                      + `Card reads: ${txt.slice(0, 240)}`);
      return `${hadPos ? "pressed the till button; " : "no till button on this card; "}`
           + `${hadAct ? "pressed the status control; " : "no status control; "}card now reads: ${txt.slice(0, 260)}`;
    });

    /* 7 — the crux. Per-ITEM serve. */
    await jr.step("WAITER", "The waiter serves ONE of my four dishes", async id => {
      /* Look wherever a waiter would: the order card first, then the table sheet. */
      let scope = "orders list";
      let perItem = await ids(w, "mark-served-btn-");
      if (!perItem.length) {
        await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
        await w.waitForTimeout(DWELL);
        await w.getByTestId(`table-card-${T}`).click({ timeout: 25000 }).catch(() => {});
        await w.waitForTimeout(DWELL);
        scope = "table sheet";
        perItem = await ids(w, "mark-served-btn-");
        notes.tableSheetIds = await allIds(w);
        jr.note(`> **On the table sheet the waiter has:** \`${notes.tableSheetIds.join("`, `")}\``);
      }
      const t = await look(w, jr, "waiter", id, `looking for a way to serve one dish (${scope})`);
      if (!perItem.length) {
        notes.serveSearch = (await allIds(w)).filter(x => /serve|deliver|item|status/i.test(x));
        jr.note(`> **Everything on screen that mentions serving, delivering, an item or a status:** `
              + `\`${notes.serveSearch.join("\`, \`") || "nothing"}\``);
      }
      if (!perItem.length)
        throw new Error("I can find no way to serve a SINGLE dish. On the orders list the only serve "
                      + `control is order-mark-served-${myOrder.slice(0, 8)}, which is the whole order. `
                      + `Screen reads: ${t.slice(0, 240)}`);
      served = perItem[0];
      await w.getByTestId(`mark-served-btn-${served}`).click({ timeout: 20000 });
      await w.waitForTimeout(DWELL);
      const conf = w.getByRole("button", { name: /^(serve|mark served|confirm|yes)/i }).first();
      if (await conf.count()) { await conf.click({ timeout: 15000 }).catch(() => {}); await w.waitForTimeout(DWELL); }
      const t2 = await look(w, jr, "waiter", id + "b", "after serving one dish");
      if (await w.getByTestId(`mark-served-btn-${served}`).count())
        throw new Error(`I pressed serve on one dish and its serve button is still there. ${t2.slice(0, 240)}`);
      return `served item ${served.slice(0, 8)} from the ${scope}. Screen: ${t2.slice(0, 240)}`;
    });

    /* 8 */
    await jr.step("PATRON", "My phone shows one dish served and three still coming", async id => {
      await p.waitForTimeout(15000);
      await p.locator('[data-testid="m2-dock-trigger"], [data-testid="m2-view-order"], [data-testid="m2-dock-cart"]')
             .first().click({ timeout: 25000 }).catch(() => {});
      await p.waitForTimeout(DWELL);
      const ordered = p.getByRole("tab", { name: /ordered/i }).or(p.getByText(/^ordered$/i)).first();
      if (await ordered.count()) { await ordered.click({ timeout: 15000 }).catch(() => {}); await p.waitForTimeout(DWELL); }
      const t = await look(p, jr, "patron", id, "my running order after one was served");
      notes.ledgerIds = await allIds(p);
      const nServed = (t.match(/served/gi) || []).length;
      if (!/served/i.test(t))
        throw new Error("a dish was served and my phone never says so. My running order reads: "
                      + t.slice(0, 300));
      return `my ledger reads: ${t.slice(0, 400)} (the word "served" appears ${nServed}x)`;
    });

    /* 9 */
    await jr.step("WAITER", "The waiter cancels a DIFFERENT one of my dishes", async id => {
      let perItem = await ids(w, "cancel-item-btn-");
      if (!perItem.length) {
        await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
        await w.waitForTimeout(DWELL);
        await w.getByTestId(`table-card-${T}`).click({ timeout: 25000 }).catch(() => {});
        await w.waitForTimeout(DWELL);
        perItem = await ids(w, "cancel-item-btn-");
      }
      const t = await look(w, jr, "waiter", id, "looking for a way to cancel one dish");
      if (!perItem.length) {
        notes.cancelSearch = (await allIds(w)).filter(x => /cancel|remove|void|item/i.test(x));
        jr.note(`> **Everything on screen that mentions cancelling, removing or an item:** `
              + `\`${notes.cancelSearch.join("\`, \`") || "nothing"}\``);
      }
      const target = perItem.filter(x => x !== served)[0];
      if (!target)
        throw new Error("I can find no way to cancel a single dish that is not the one already served. "
                      + `Screen reads: ${t.slice(0, 240)}`);
      cancelled = target;
      await w.getByTestId(`cancel-item-btn-${cancelled}`).click({ timeout: 20000 });
      await w.waitForTimeout(DWELL);
      const conf = w.getByRole("button", { name: /^(remove|cancel item|confirm|yes)/i }).first();
      if (await conf.count()) { await conf.click({ timeout: 15000 }).catch(() => {}); await w.waitForTimeout(DWELL); }
      const t2 = await look(w, jr, "waiter", id + "b", "after cancelling one dish");
      if (await w.getByTestId(`cancel-item-btn-${cancelled}`).count())
        throw new Error(`I pressed cancel and that dish still has its cancel button. ${t2.slice(0, 240)}`);
      return `cancelled item ${cancelled.slice(0, 8)}. Screen: ${t2.slice(0, 240)}`;
    });

    /* 10 — the whole point of the journey. */
    await jr.step("PATRON", "My phone shows all three at once: served, cancelled, still coming", async id => {
      await p.waitForTimeout(15000);
      const t = await look(p, jr, "patron", id, "served + cancelled + ordered together");
      const hasServed = /served/i.test(t), hasCancel = /cancel/i.test(t);
      const hasPending = /(ordered|in the kitchen|on its way|preparing)/i.test(t);
      notes.mixedLedger = t.slice(0, 600);
      if (!(hasServed && hasCancel && hasPending))
        throw new Error("my ledger does not show all three states together. served="
          + `${hasServed}, cancelled=${hasCancel}, still-coming=${hasPending}. It reads: ${t.slice(0, 400)}`);
      return `all three states are on screen together: ${t.slice(0, 400)}`;
    });

    /* 11 */
    await jr.step("PATRON", "I reload the page and the same three states are still there", async id => {
      await p.reload({ waitUntil: "domcontentloaded" });
      await p.waitForTimeout(DWELL + 8000);
      const gates = await arriveAsDiner(p, RID, T, HOUSE.layout).catch(() => []);
      await p.locator('[data-testid="m2-dock-trigger"], [data-testid="m2-view-order"], [data-testid="m2-dock-cart"]')
             .first().click({ timeout: 25000 }).catch(() => {});
      await p.waitForTimeout(DWELL);
      const ordered = p.getByRole("tab", { name: /ordered/i }).or(p.getByText(/^ordered$/i)).first();
      if (await ordered.count()) { await ordered.click({ timeout: 15000 }).catch(() => {}); await p.waitForTimeout(DWELL); }
      const t = await look(p, jr, "patron", id, "same ledger after a hard reload");
      const ok = /served/i.test(t) && /cancel/i.test(t) && /(ordered|in the kitchen|on its way|preparing)/i.test(t);
      if (!ok)
        throw new Error("after reloading, my ledger no longer shows the three states. It reads: "
                      + t.slice(0, 400));
      return `after the reload it still reads: ${t.slice(0, 400)}`;
    });

    /* 12 */
    await jr.step("WAITER", "The waiter serves the last two dishes", async id => {
      await w.reload({ waitUntil: "domcontentloaded" });
      await w.waitForTimeout(DWELL + 3000);
      await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      await w.getByTestId(`table-card-${T}`).click({ timeout: 25000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      let left = await ids(w, "mark-served-btn-");
      const n0 = left.length;
      for (const it of left) {
        await w.getByTestId(`mark-served-btn-${it}`).click({ timeout: 20000 }).catch(() => {});
        await w.waitForTimeout(3000);
        const conf = w.getByRole("button", { name: /^(serve|mark served|confirm|yes)/i }).first();
        if (await conf.count()) { await conf.click({ timeout: 15000 }).catch(() => {}); await w.waitForTimeout(3000); }
      }
      await w.waitForTimeout(DWELL);
      const t = await look(w, jr, "waiter", id, "after serving the rest");
      const remaining = (await ids(w, "mark-served-btn-")).length;
      if (remaining)
        throw new Error(`I served every dish I could and ${remaining} still have a serve button. ${t.slice(0, 240)}`);
      return `served the remaining ${n0}. Screen: ${t.slice(0, 240)}`;
    });

    /* 13 */
    await jr.step("PATRON", "My phone finally reads three served and one cancelled", async id => {
      await p.waitForTimeout(15000);
      const t = await look(p, jr, "patron", id, "the finished ledger");
      const nServed = (t.match(/served/gi) || []).length;
      notes.finalLedger = t.slice(0, 600);
      if (!/cancel/i.test(t))
        throw new Error(`the cancelled dish has disappeared from my ledger. It reads: ${t.slice(0, 400)}`);
      if (/\b(ordered|in the kitchen|on its way|preparing)\b/i.test(t) && nServed < 2)
        throw new Error("everything has been served but my phone still says dishes are coming. "
                      + t.slice(0, 400));
      return `final ledger: ${t.slice(0, 400)}`;
    });

    verdict = "HONEST_GREEN";
    why = `Walked all thirteen steps on Canary Mosaic table ${T} with the fixture's own waiter: `
        + "four dishes ordered, one served, one cancelled, the diner's phone showed served, "
        + "cancelled and still-coming together and kept them across a hard reload.";
  } catch (e) {
    why = String(e.message || e).split("\n")[0];
  } finally {
    fs.writeFileSync(`${jr.dir}/NOTES.json`, JSON.stringify({ dishes, myOrder, served, cancelled, ...notes }, null, 1));
    /* Give the table back. A walk that poisons its own runway ruins the next one. */
    try {
      await w.getByTestId("tab-tables").click({ timeout: 15000 }).catch(() => {});
      await w.waitForTimeout(3000);
      await w.getByTestId(`table-card-${T}`).click({ timeout: 15000 }).catch(() => {});
      await w.waitForTimeout(3000);
      await w.getByTestId("table-detail-tab-bill").click({ timeout: 10000 }).catch(() => {});
      await w.waitForTimeout(2000);
      const label = w.getByText(/swipe to close/i).first();
      if (await label.count()) {
        const b = await label.boundingBox();
        if (b) {
          const y = b.y + b.height / 2, x0 = Math.max(12, b.x - 24), x1 = b.x + b.width + 60;
          await w.mouse.move(x0, y); await w.mouse.down();
          for (let i = 1; i <= 14; i++) { await w.mouse.move(x0 + ((x1 - x0) * i) / 14, y); await w.waitForTimeout(45); }
          await w.mouse.up(); await w.waitForTimeout(DWELL);
        }
      }
      await w.reload({ waitUntil: "domcontentloaded" }); await w.waitForTimeout(DWELL);
      await w.getByTestId("tab-tables").click({ timeout: 15000 }).catch(() => {});
      await w.waitForTimeout(3000);
      const card = await w.getByTestId(`table-card-${T}`).innerText().catch(() => "");
      await look(w, jr, "waiter", "z9", "board after I tried to give the table back");
      leftBehind = /no guests/i.test(card)
        ? `nothing — table ${T} reads "No guests" again`
        : `table ${T} still reads "${card.replace(/\s+/g, " ").slice(0, 90)}" — I could not close it`;
    } catch { leftBehind += " (I could not reach the close control)"; }
    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");
});
