/* WITNESS · mosaic_v2-cancel   (ledger W-030)
   The promise: a waiter can remove a dish a diner ordered, and the diner is told.

   WHERE: Anant, table 10 (daniel's, currently empty). NOT the canary fixture - no canary
   waiter credential works in this container, which is why this journey sat at partial green.
   Table 10 rather than table 2 because table 2 is already stuck: two served items there and no
   control anywhere that settles the bill, so its close is locked (see W-063).

   WHY THIS WALK MATTERS BEYOND ITS OWN ROW: the waiter app's own instruction is "move all
   ordered items to served state or Canceled (Remove)". I took the SERVED branch on table 2 and
   the close never unlocked. This walk takes the CANCELED branch. If cancelling settles the
   bill, the dead-end is specific to serving; if it does not, no branch of the app's own
   instruction works. Either answer is worth having.                                        */
import { test, expect } from "@playwright/test";
import { journal, look, arriveAsDiner, signInWaiter, DWELL,
         roleContext, closeRole, account, PATRON } from "../lib-witness.mjs";

const RID   = "097c229c-a1cb-4ba7-b603-800e46936133";   // Anant
const TABLE = 10;

test("mosaic_v2-cancel", async ({ browser }) => {
  const jr = journal("anant-cancel");
  jr.target({ app: "patron (mosaic_v2) + waiter", baseUrl: PATRON, restaurantId: RID, table: TABLE,
              substitution: "Anant + daniel@, NOT the canary fixture - no canary waiter credential exists here" });
  jr.note("**Where:** Anant, table 10. **Not** the canary's own fixture — this grades the *feature*, "
        + "not Canary Mosaic V2's own state.");
  jr.note("**Also answering:** whether the *Canceled (Remove)* branch of the waiter app's own "
        + "settle instruction unlocks the table close, when the *Served* branch did not (see W-063).");

  const cP = await roleContext(browser, jr, "patron");
  const cW = await roleContext(browser, jr, "waiter");
  const p = await cP.newPage(), w = await cW.newPage();
  let verdict = "INCOMPLETE", why = "", leftBehind = "nothing", itemId = "", dish = "";

  try {
    await jr.step("PATRON", `I sit at table ${TABLE} and order a drink`, async id => {
      const gates = await arriveAsDiner(p, RID, TABLE, "mosaic_v2");
      const add = p.locator('[data-testid^="m2-add-"]').first();
      await expect(add, "there should be something I can order").toBeVisible({ timeout: 25000 });
      dish = (await p.locator('[data-testid^="m2-text-"]').first().innerText())
               .replace(/\s+/g, " ").slice(0, 40).trim();
      await add.click({ timeout: 25000 });
      await p.waitForTimeout(DWELL);
      await p.locator('[data-testid="m2-view-order"], [data-testid="m2-dock-cart"]')
             .first().click({ timeout: 25000 });
      await p.waitForTimeout(DWELL);
      await p.locator('[data-testid="order-now-btn"]').first().click({ timeout: 25000 });
      const t = await look(p, jr, "patron", id, "order placed");
      leftBehind = `a live session and one order at table ${TABLE}`;
      return `passed: ${gates.join(", ") || "no gates"}. Ordered "${dish}". Screen: ${t.slice(0, 150)}`;
    });

    await jr.step("WAITER", "Daniel opens my table and finds my dish with a way to remove it", async id => {
      const acc = account("waiter");
      await signInWaiter(w, acc.email, acc.password);
      await w.getByTestId("tab-tables").click({ timeout: 20000 }).catch(() => {});
      await w.waitForTimeout(DWELL);
      await w.getByTestId(`table-card-${TABLE}`).click({ timeout: 25000 });
      await w.waitForTimeout(DWELL);
      await expect(w.getByTestId("table-detail-sheet")).toBeVisible({ timeout: 20000 });
      const t = await look(w, jr, "waiter", id, "guests tab with my order");
      const cancel = w.locator('[data-testid^="cancel-item-btn-"]').first();
      await expect(cancel, "there should be a way to remove the dish").toBeVisible({ timeout: 20000 });
      itemId = (await cancel.getAttribute("data-testid")).replace("cancel-item-btn-", "");
      return `sheet reads: ${t.slice(0, 220)}`;
    });

    await jr.step("WAITER", "He removes the dish, and it goes", async id => {
      await w.locator(`[data-testid="cancel-item-btn-${itemId}"]`).click({ timeout: 20000 });
      await w.waitForTimeout(DWELL);
      /* A confirm may appear. Take it if it does - a waiter would - and say so either way. */
      const confirm = w.getByRole("button", { name: /^(remove|cancel item|confirm|yes)/i }).first();
      const hadConfirm = await confirm.count() > 0;
      if (hadConfirm) { await confirm.click({ timeout: 15000 }).catch(() => {}); await w.waitForTimeout(DWELL); }
      const t = await look(w, jr, "waiter", id, "after removing the dish");
      /* Positive evidence: the remove control for THAT item is gone. "No error" would pass on
         a button that did nothing at all. */
      const stillThere = await w.locator(`[data-testid="cancel-item-btn-${itemId}"]`).count();
      if (stillThere)
        throw new Error("I pressed remove and the dish is still there with its remove button. "
                      + `Sheet reads: ${t.slice(0, 220)}`);
      return `${hadConfirm ? "confirmed the removal; " : "no confirmation was asked; "}`
           + `the dish and its remove control are gone. Sheet: ${t.slice(0, 200)}`;
    });

    await jr.step("PATRON", "My phone tells me the dish was removed", async id => {
      await p.waitForTimeout(15000);
      const t = await look(p, jr, "patron", id, "diner screen after the removal");
      const told = /cancel|removed|no longer|unavailable/i.test(t);
      if (!told)
        throw new Error("the waiter removed my dish and my phone never said so. It reads: "
                      + t.slice(0, 220));
      return `my screen reads: ${t.slice(0, 240)}`;
    });

    await jr.soft("WAITER", "With nothing left on it, the table can now be closed", async id => {
      await w.getByTestId("table-detail-tab-bill").click({ timeout: 20000 }).catch(() => {});
      const t = await look(w, jr, "waiter", id, "bill tab after the removal");
      if (/swipe after bill is settled/i.test(t))
        throw new Error("even with every item removed, the close still reads 'Swipe after bill is "
                      + `settled'. Bill tab: ${t.slice(0, 240)}`);
      expect(t, "the close should be offered").toMatch(/swipe to close/i);
      const label = w.getByText(/swipe to close/i).first();
      const box = await label.boundingBox();
      if (box) {
        const y = box.y + box.height / 2, x0 = Math.max(12, box.x - 24), x1 = box.x + box.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: 20000 }).catch(() => {});
      const card = (await w.getByTestId(`table-card-${TABLE}`).innerText()).replace(/\s+/g, " ");
      await look(w, jr, "waiter", id, "board after closing");
      if (/no guests/i.test(card)) leftBehind = `nothing — table ${TABLE} reads 'No guests' again`;
      return `Table ${TABLE} now reads "${card}"`;
    });

    verdict = "HONEST_GREEN";
    why = `Walked end to end on Anant table ${TABLE} with a real waiter: the diner ordered, Daniel `
        + "removed the dish from the table sheet, and the diner's phone was told.";
  } catch (e) {
    why = String(e.message || e).split("\n")[0];
  } finally {
    jr.note(`> **What I left on table ${TABLE}:** ${leftBehind}.`);
    jr.finish(verdict, why);
    await closeRole(cP, jr, "patron");
    await closeRole(cW, jr, "waiter");
  }
  expect(verdict, why).toBe("HONEST_GREEN");
});
