/* WITNESS · mosaic_v2-enter-pos (W-044) — QRATE-UAT-1
 *
 * The promise, in a person's words: I order at a table; a member of staff confirms it; while
 * it is only confirmed I can still cancel my dish; the moment staff enter it into the till
 * that cancel disappears - and stays gone when I reload - and the kitchen screen is gated on
 * that same moment.
 *
 * SUBSTITUTION, stated up front: this journey names a WAITER. No waiter account in this
 * container can see a canary restaurant (the canary waiter logins are refused, and the owner
 * has no table assignment, so the waiter board renders "No tables assigned"). The owner app's
 * own orders board DOES show these orders and offers Confirm / Cancel, so that is the staff
 * actor I use. It is not the waiter app the journey names, and I say so in the verdict.
 *
 * CLEANUP: I cancel my own dish at the end, so this table is not left holding a live order.
 */
import { test } from "@playwright/test";
import { journal, look, roleContext, deskContext, closeRole, signInOwner, account,
         arriveAsDiner, PATRON, OWNER, DWELL } from "../lib-witness.mjs";
import fs from "node:fs";

const F = JSON.parse(fs.readFileSync("/workspace/uat/runs/wave0/fixtures.json", "utf8"));
const GATED = F["pos-gate"];          // the journey's own fixture — has NO menu
const RID   = F["canary-mosaic"];     // where a person can actually order
const TABLE = 9951;
const OUT = "/workspace/uat/wave1/session-recon";

const tap = async (page, sel) => {
  const l = page.locator(sel).first();
  if (!(await l.count())) return false;
  await l.click({ timeout: 10000 }).catch(async () => { await l.evaluate(el => el.click()); });
  return true;
};
/** The post-order ledger: open the cart sheet and switch to the Ordered tab. */
async function openLedger(page) {
  await tap(page, '[data-testid="m2-dock-trigger"]'); await page.waitForTimeout(1800);
  await tap(page, '[data-testid="m2-dock-cart"]');    await page.waitForTimeout(DWELL);
  await tap(page, '[data-testid="m2-cart-tab-table"]'); await page.waitForTimeout(3000);
  return {
    cancels: await page.locator('[data-testid^="cancel-item-"]').count(),
    badge: (await page.locator('[data-testid="order-status-badge"]').first().innerText().catch(() => "")).replace(/\s+/g, " ").trim(),
    sheet: (await page.locator('[data-testid="cart-sheet"]').first().innerText().catch(() => "")).replace(/\s+/g, " ").trim().slice(0, 300),
  };
}
const vis = page => page.evaluate(() =>
  [...document.querySelectorAll("button,[role=button],a[href],[role=menuitem]")]
    .filter(e => e.offsetParent !== null)
    .map(e => ({ t: (e.innerText || "").replace(/\s+/g, " ").trim().slice(0, 40), tid: e.getAttribute("data-testid") })));

test("mosaic_v2-enter-pos", async ({ browser }) => {
  const jr = journal("mosaic_v2-enter-pos");
  jr.target({ app: "patron+owner", patronUrl: PATRON, ownerUrl: OWNER, restaurantId: RID,
              table: TABLE, fixture: "Canary Mosaic V2 POS Gate — dev",
              substitution: "owner orders board stands in for the WAITER actor" });
  jr.note("_A diner orders; staff confirm; the diner can still cancel; entering the order into "
        + "the till is supposed to take that cancel away._");
  jr.note("> **Substitution:** the journey names a WAITER. No waiter login in this container "
        + "can see a canary restaurant, so the staff actor here is the OWNER's own orders "
        + "board, which shows these orders and offers Confirm / Cancel.");

  const pc = await roleContext(browser, jr, "patron");
  const p = await pc.newPage();
  const oc = await deskContext(browser, jr, "owner");
  const o = await oc.newPage();
  const rec = {};
  let verdict = "INCOMPLETE", why = "", placed = false;

  try {
    await jr.step("PATRON", "First I try the journey's OWN fixture, the POS-gated one", async id => {
      await arriveAsDiner(p, GATED, TABLE);
      const t = await look(p, jr, "patron", id, "the POS Gate restaurant");
      const tiles = await p.locator('[data-testid^="m2-tile-"]').count();
      const empty = await p.locator('[data-testid="layout-empty-state"]').count();
      const sections = await p.locator('[data-testid^="m2-section-"]').count();
      rec.gatedFixture = { tiles, empty, sections, text: t.slice(0, 200) };
      jr.note(`> **Canary Mosaic V2 POS Gate as a diner sees it:** ${tiles} dishes, ${sections} courses, `
            + `\`layout-empty-state\` present ${empty ? "YES" : "no"}. The whole screen is ${t.length} characters: "${t.slice(0, 120)}"`);
      return `Nothing to order: the app renders its own empty state and there is not one dish or even a course to tap. `
           + `So I cannot walk this journey on its own fixture, and I move to Canary Mosaic, which has a real menu.`;
    });

    await jr.step("PATRON", "I sit down at a table I can actually order from and the menu loads", async id => {
      const seen = await arriveAsDiner(p, RID, TABLE);
      const t = await look(p, jr, "patron", id, "joined the table");
      /* mosaic_v2 opens on the DRINKS course. Counting tiles on arrival counts drinks only,
         so before I say "empty" I open every course this restaurant actually renders. */
      rec.sections = await p.evaluate(() => [...document.querySelectorAll('[data-testid^="m2-section-"]')]
        .map(e => ({ tid: e.getAttribute("data-testid"), label: (e.innerText || "").replace(/\s+/g, " ").trim().slice(0, 24) })));
      rec.byCourse = {};
      rec.tiles = await p.locator('[data-testid^="m2-tile-"]').count();
      rec.byCourse["(as it opened)"] = rec.tiles;
      for (const sec of rec.sections) {
        await p.locator(`[data-testid="${sec.tid}"]`).first().evaluate(el => el.click()).catch(() => {});
        await p.waitForTimeout(3500);
        rec.byCourse[sec.label || sec.tid] = await p.locator('[data-testid^="m2-tile-"]').count();
      }
      jr.note(`> **Every course on this restaurant's menu:** ${JSON.stringify(rec.byCourse)}`);
      const best = Object.entries(rec.byCourse).filter(([k]) => k !== "(as it opened)").sort((a, b) => b[1] - a[1])[0];
      if (best && best[1] > 0) {
        const sec = rec.sections.find(x => (x.label || x.tid) === best[0]);
        await p.locator(`[data-testid="${sec.tid}"]`).first().evaluate(el => el.click()).catch(() => {});
        await p.waitForTimeout(3500);
      }
      rec.tiles = await p.locator('[data-testid^="m2-tile-"]').count();
      await look(p, jr, "patron", `${id}b`, "best course opened");
      if (!rec.tiles) throw new Error(`Every course on this restaurant's menu is empty - ${JSON.stringify(rec.byCourse)} - so there is nothing to order.`);
      return `${rec.tiles} dishes on screen once I open the fullest course. Per course: ${JSON.stringify(rec.byCourse)}. `
           + `Screen: ${t.slice(0, 120)}`;
    });

    await jr.step("PATRON", "I add a dish to my cart", async id => {
      await p.locator('[data-testid^="m2-tile-"]').first().evaluate(el => el.click());
      await p.waitForTimeout(DWELL);
      await p.locator('[data-testid="composition-add"]').first().click({ timeout: 20000 });
      await p.waitForTimeout(DWELL);
      await look(p, jr, "patron", id, "one in the cart");
      const n = (await p.locator('[data-testid="m2-dock-cart-count"]').first().innerText().catch(() => "")).trim();
      if (!n || n === "0") throw new Error("Nothing landed in the cart.");
      return `The cart pill reads ${n}.`;
    });

    await jr.step("PATRON", "I send the order to the kitchen", async id => {
      await tap(p, '[data-testid="m2-dock-trigger"]'); await p.waitForTimeout(1800);
      await tap(p, '[data-testid="m2-dock-cart"]');    await p.waitForTimeout(DWELL);
      const btn = p.locator('[data-testid="order-now-btn"]').first();
      if (!(await btn.count())) throw new Error("There is no Place order button in my cart.");
      const label = (await btn.innerText()).replace(/\s+/g, " ").trim();
      /* The cart sheet's scrim covers the button's hit-box, so an ordinary click times out
         with "subtree intercepts pointer events". The evaluate-click is the idiom this repo
         already uses for pointer-intercepted controls - it is still the button's own handler. */
      await tap(p, '[data-testid="order-now-btn"]');
      await p.waitForTimeout(DWELL * 2);
      const t = await look(p, jr, "patron", id, "order placed");
      if (await p.locator('[data-testid="signup-gate"]').count())
        throw new Error("It asked who I am instead of sending the order.");
      placed = true;
      const led = await openLedger(p);
      rec.afterPlace = led;
      await look(p, jr, "patron", id, "the Ordered tab");
      if (!led.cancels && !/ordered/i.test(led.sheet))
        throw new Error(`I cannot tell the order was sent — the Ordered tab reads "${led.sheet}"`);
      return `I pressed "${label}". The Ordered tab now reads "${led.sheet}" with badge "${led.badge}" `
           + `and ${led.cancels} cancel control(s).`;
    });

    await jr.step("OWNER", "Staff confirm my order (owner orders board, standing in for the waiter)", async id => {
      const a = account("canaryOwner");
      await signInOwner(o, a.email, a.password, RID);
      await o.goto(`${OWNER}/owner/orders/?restaurantId=${RID}`, { waitUntil: "domcontentloaded" });
      await o.waitForTimeout(DWELL + 3000);
      const t0 = await look(o, jr, "owner", id, "orders board");
      if (!new RegExp(`Table ${TABLE}\\b`).test(t0))
        throw new Error(`My order is not on the staff board at all — no "Table ${TABLE}" on it. Board reads: ${t0.slice(0, 220)}`);
      // press the Confirm that belongs to MY table's card, not the first Confirm on the page
      const ok = await o.evaluate((tbl) => {
        const label = [...document.querySelectorAll("*")].find(e => e.children.length === 0 && new RegExp(`^Table ${tbl}$`).test((e.innerText || "").trim()));
        if (!label) return "no card";
        let card = label;
        for (let i = 0; i < 8 && card; i++, card = card.parentElement) {
          const b = [...card.querySelectorAll("button")].find(x => /^confirm$/i.test((x.innerText || "").trim()));
          if (b) { b.click(); return "clicked"; }
        }
        return "no confirm button on my card";
      }, TABLE);
      await o.waitForTimeout(DWELL + 3000);
      const t1 = await look(o, jr, "owner", id, "after confirming");
      rec.confirm = { result: ok, before: t0.slice(0, 400), after: t1.slice(0, 400) };
      if (ok !== "clicked") throw new Error(`I could not confirm the order: ${ok}.`);
      return `Confirmed. The board around my table now reads: "${(t1.match(new RegExp(`Table ${TABLE}[\\s\\S]{0,120}`)) || [""])[0].replace(/\\s+/g, " ")}"`;
    });

    await jr.step("PATRON", "While it is only confirmed, I can still cancel my dish", async id => {
      await p.reload({ waitUntil: "domcontentloaded" });
      await p.waitForTimeout(DWELL + 3000);
      const led = await openLedger(p);
      await look(p, jr, "patron", id, "cancel still offered");
      rec.cancelVisible = led;
      if (!led.cancels) throw new Error(`No cancel control is offered. The Ordered tab reads "${led.sheet}".`);
      return `${led.cancels} cancel control(s) offered; badge "${led.badge}"; sheet "${led.sheet.slice(0, 160)}".`;
    });

    await jr.step("OWNER", "Now staff enter the order into the till — I go looking for that control", async id => {
      const screens = [];
      // the restaurant's OWN sidebar first, then the order itself
      await o.goto(`${OWNER}/owner/dashboard/?restaurantId=${RID}`, { waitUntil: "domcontentloaded" });
      await o.waitForTimeout(DWELL);
      const nav = await o.evaluate(() => [...document.querySelectorAll("nav a[href], aside a[href], a[data-testid^=nav-]")]
        .filter(e => e.offsetParent !== null)
        .map(e => ({ label: (e.innerText || "").replace(/\s+/g, " ").trim().slice(0, 30), href: e.getAttribute("href") })));
      jr.note(`> **This restaurant's sidebar offers:** ${[...new Set(nav.map(n => n.label))].join(" · ")}`);
      for (const href of [...new Set(nav.map(n => n.href).filter(Boolean))].concat([`/owner/settings/pos/?restaurantId=${RID}`, `/owner/orders/?restaurantId=${RID}`])) {
        await o.goto(new URL(href, OWNER).href, { waitUntil: "domcontentloaded" }).catch(() => {});
        await o.waitForTimeout(3500);
        const cs = await vis(o);
        const hits = cs.filter(c => /pos|till|enter|kds|kitchen ?display|push|send to/i.test(`${c.t} ${c.tid || ""}`) && !/^Canary/.test(c.t));
        screens.push({ href, hits: hits.map(h => `"${h.t}"${h.tid ? `(${h.tid})` : ""}`) });
      }
      // and inside my own order card
      await o.goto(`${OWNER}/owner/orders/?restaurantId=${RID}`, { waitUntil: "domcontentloaded" });
      await o.waitForTimeout(DWELL + 2000);
      await o.evaluate((tbl) => {
        const el = [...document.querySelectorAll("*")].find(e => e.children.length === 0 && new RegExp(`^Table ${tbl}$`).test((e.innerText || "").trim()));
        if (el) { let a = el; for (let i = 0; i < 5 && a; i++, a = a.parentElement) {} el.click(); }
      }, TABLE);
      await o.waitForTimeout(DWELL);
      const t = await look(o, jr, "owner", id, "looking for a POS control");
      const inOrder = (await vis(o)).filter(c => /pos|till|enter|kds|push/i.test(`${c.t} ${c.tid || ""}`) && !/^Canary/.test(c.t));
      rec.posSearch = { screens, inOrder: inOrder.map(h => `"${h.t}"${h.tid ? `(${h.tid})` : ""}`) };
      fs.writeFileSync(`${OUT}/w044-pos-search.json`, JSON.stringify(rec.posSearch, null, 1));
      jr.note("> **Every owner screen searched for a way to enter an order into the till:**\n>\n> - "
            + screens.map(s => `\`${s.href.split("?")[0]}\` → ${s.hits.length ? s.hits.join(", ") : "nothing"}`).join("\n> - "));
      const any = screens.some(s => s.hits.length) || inOrder.length;
      if (!any)
        throw new Error("There is no control anywhere in the owner app that enters an order into the till. "
          + `I walked all ${screens.length} screens in this restaurant's own navigation and opened the order itself; `
          + "the only actions offered on an order are Confirm and Cancel. The waiter app, which might have one, "
          + "renders \"No tables assigned\" for every canary restaurant with the only staff login this container holds.");
      return `Found: ${screens.filter(s => s.hits.length).map(s => `${s.href} → ${s.hits.join(",")}`).join(" ; ")} ${rec.posSearch.inOrder.join(",")}`;
    });

    verdict = "WALKED"; why = "walked the whole journey";
    jr.finish(verdict, why);
  } catch (e) {
    why = String(e.message || e).split("\n")[0];
    jr.finish("STOPPED", why);
  } finally {
    // ---- cleanup: I do not leave a live order sitting on somebody's table ----
    if (placed) {
      await jr.soft("PATRON", "CLEANUP — I cancel my own dish so nothing is left on this table", async id => {
        await p.reload({ waitUntil: "domcontentloaded" });
        await p.waitForTimeout(DWELL + 2000);
        const led = await openLedger(p);
        if (!led.cancels) return `No cancel control is offered any more, so I cannot clear it. Table ${TABLE} is left holding one confirmed order.`;
        await p.locator('[data-testid^="cancel-item-"]').first().click({ timeout: 15000 }).catch(async () => {
          await p.locator('[data-testid^="cancel-item-"]').first().evaluate(el => el.click());
        });
        await p.waitForTimeout(2500);
        const confirm = p.getByRole("button", { name: /^(remove|cancel item|confirm|yes|cancel order)/i }).first();
        if (await confirm.count()) { await confirm.click({ timeout: 12000 }).catch(() => {}); await p.waitForTimeout(DWELL); }
        const after = await openLedger(p);
        await look(p, jr, "patron", id, "after cancelling my dish");
        rec.cleanup = after;
        return `Cancel controls before ${led.cancels} → after ${after.cancels}; the sheet now reads "${after.sheet.slice(0, 160)}".`;
      });
      jr.finish(verdict === "WALKED" ? "WALKED" : "STOPPED", why + " (cleanup attempted — see the last step)");
    }
    fs.writeFileSync(`${OUT}/w044.json`, JSON.stringify(rec, null, 1));
    await closeRole(pc, jr, "patron");
    await closeRole(oc, jr, "owner");
  }
});
