/* WITNESS · W-026 mosaic-v2-edit — "I can change my mind about a dish I already ordered"
 *
 * WHAT THE JOURNEY PROMISES (its own step list, not the screen):
 *   4  I open a dish, set the quantity to 2, and add it
 *   5  I reopen it and it is PRE-FILLED with what I chose — the step calls this "(edit)"
 *   6  I add it again and my order still has ONE line for it, updated in place
 *
 * Steps 1-2 (owner auth, create a plain canary dish) and 7 (teardown) are the canary's own
 * provisioning. I create nothing: a plain canary dish already exists on this menu exactly as
 * step 2 describes, and making another on a live fixture mid-run would hand the builder a
 * false red. So this walk covers steps 3-6 and says nothing about 1, 2 or 7.
 *
 * Step 3 says "Patron Auth + Profile". I walk it as a GUEST, the app's default diner path.
 * The behaviour under test is the order's, not the sign-in's — but I did not exercise the
 * signed-in path and claim nothing about it.
 *
 * THE QUESTION THIS WALK IS BUILT AROUND: step 5 pre-fills the sheet with the quantity I
 * already have, which tells me I am EDITING. Step 6 then presses Add. So — does Add treat
 * that pre-filled number as "make it this" or as "add this many more"? To answer it without
 * ambiguity I press Add having changed NOTHING. If editing 2 and confirming 2 leaves me
 * with 2, the sheet is an editor. If it leaves me with 4, the pre-fill is lying to me.
 *
 * TRAPS AVOIDED: the menu paints THROUGH the sheet, so every read is scoped to the sheet or
 * the cart's own container; and "exactly one line" is never proved by a poll alone — a poll
 * returns the instant it matches and is blind to an overshoot landing a beat later, so I
 * poll, settle, then re-read.
 */
import { test, expect } from "@playwright/test";
import fs from "node:fs";
import { journal, look, arriveAsDiner, roleContext, closeRole, DWELL, PATRON }
  from "../lib-witness.mjs";

const RID   = "f469b191-ec79-585d-916a-e6c497617eb8";   // Canary Mosaic V2 Dining — dev
const TABLE = 9932;
const STRAY = [9926, 9931];                              // tables my earlier attempts left dirty
const DISH  = "Canary Caesar Salad";
const UNIT  = 8.99;

const sheet = p => p.locator('[data-testid="m2-composition"]');

/** Open the order the way a diner does: press the dock, then the trolley. */
async function openCart(p) {
  const trigger = p.locator('[data-testid="m2-dock-trigger"]').first();
  if (await trigger.count()) {
    await trigger.click({ timeout: 15000 }).catch(async () => { await trigger.evaluate(el => el.click()); });
    await p.waitForTimeout(2500);
  }
  const cartBtn = p.locator('[data-testid="m2-dock-cart"]').first();
  if (await cartBtn.count()) {
    await cartBtn.click({ timeout: 15000 }).catch(async () => { await cartBtn.evaluate(el => el.click()); });
    await p.waitForTimeout(3500);
  }
  const s = p.locator('[data-testid="cart-sheet"]').first();
  return (await s.count()) ? s : null;
}

/** What my order actually says — read ONLY inside the order sheet. */
async function readOrder(cart) {
  const text = (await cart.innerText()).replace(/\s+/g, " ").trim();
  const lines = (text.match(new RegExp(DISH, "g")) || []).length;
  const total = (text.match(/TOTAL \$([\d.]+)/i) || [])[1];
  return { lines, total: total ? Number(total) : null, text };
}

const addLabel = async p =>
  (await sheet(p).locator('[data-testid="composition-add"]').first().innerText())
    .replace(/\s+/g, " ").trim();
const sheetQty = async p =>
  (await sheet(p).locator('[data-testid="m2-comp-qty"]').first().innerText()).trim();

/** Put a table back the way I found it, using the minus on the dish tile. */
async function emptyTable(p, rid, table) {
  await arriveAsDiner(p, rid, table, "mosaic_v2");
  await p.locator('[data-testid="m2-section-Appetizers"]').first().evaluate(el => el.click())
    .catch(() => {});
  await p.waitForTimeout(3000);
  for (let i = 0; i < 20; i++) {
    const dec = p.locator('[data-testid^="m2-dec-"]').first();
    if (!(await dec.count())) break;
    await dec.click({ timeout: 8000 }).catch(async () => { await dec.evaluate(el => el.click()); });
    await p.waitForTimeout(1200);
  }
  const left = await p.locator('[data-testid^="m2-qty-"]').first().innerText().catch(() => "0");
  return (await p.locator('[data-testid^="m2-dec-"]').count()) ? `still ${left.trim()}` : "empty";
}

test("mosaic-v2-edit", async ({ browser }) => {
  const jr = journal("mosaic-v2-edit");
  jr.target({ app: "patron (mosaic_v2)", baseUrl: PATRON, restaurantId: RID, table: TABLE,
              restaurant: "Canary Mosaic V2 Dining — dev", dish: DISH });
  jr.note("**Where:** Canary Mosaic V2 Dining — dev, table 9932, iPhone 14 Pro.");
  jr.note("**Who:** a guest diner ordering two of something, then changing their mind.");
  jr.note("_The board names no restaurant for this journey. I drove it on the fixture that "
        + "carries a plain canary dish exactly as step 2 describes and whose owner dish "
        + "library actually loads. I created nothing and placed no order._");

  const c = await roleContext(browser, jr, "diner");
  const p = await c.newPage();
  const ev = {};
  let verdict = "INCOMPLETE", why = "", cleanup = "none";

  try {
    await jr.step("PATRON", "I sit down and the menu opens", async id => {
      const gates = await arriveAsDiner(p, RID, TABLE, "mosaic_v2");
      const t = await look(p, jr, "diner", id, "menu");
      return `passed through: ${gates.join(", ") || "no gates"}. ${t.slice(0, 130)}`;
    });

    await jr.step("PATRON", "I move off drinks and open the salad", async id => {
      await p.locator('[data-testid="m2-section-Appetizers"]').first().evaluate(el => el.click());
      await p.waitForTimeout(DWELL);
      await p.locator('[data-testid^="m2-tile-"]').first().click({ timeout: 20000 });
      await p.waitForTimeout(DWELL);
      if (!(await sheet(p).count())) throw new Error("tapping the dish opened nothing");
      ev.firstOpen = { qty: await sheetQty(p), add: await addLabel(p) };
      await look(p, jr, "diner", id, "dish open");
      return `It opens showing ${ev.firstOpen.qty}, and the button says "${ev.firstOpen.add}".`;
    });

    await jr.step("PATRON", "I make it 2 and add it", async id => {
      await sheet(p).locator('[data-testid="m2-comp-inc"]').first().click({ timeout: 15000 });
      await p.waitForTimeout(1500);
      ev.beforeFirstAdd = { qty: await sheetQty(p), add: await addLabel(p) };
      await look(p, jr, "diner", id, "qty 2");
      if (ev.beforeFirstAdd.qty !== "2")
        throw new Error(`the + left it reading "${ev.beforeFirstAdd.qty}", not 2`);
      await sheet(p).locator('[data-testid="composition-add"]').first().click({ timeout: 20000 });
      await p.waitForTimeout(DWELL);
      return `It read 2 and the button said "${ev.beforeFirstAdd.add}". I pressed it.`;
    });

    await jr.step("PATRON", "my order has ONE line of it, two of them, $17.98", async id => {
      const cart = await openCart(p);
      if (!cart) throw new Error("I could not find anywhere to look at my order");
      await expect.poll(async () => (await readOrder(cart)).lines, { timeout: 15000 }).toBeGreaterThan(0);
      await p.waitForTimeout(750);                       // settle: a poll cannot see an overshoot
      const o = await readOrder(cart);
      ev.afterFirstAdd = o;
      await look(p, jr, "diner", id, "order after first add");
      cleanup = `an unplaced order on table ${TABLE}`;
      if (o.lines !== 1) throw new Error(`${o.lines} lines, not 1: ${o.text.slice(0, 300)}`);
      if (o.total !== +(UNIT * 2).toFixed(2))
        throw new Error(`the total is $${o.total}, not $${(UNIT * 2).toFixed(2)}: ${o.text.slice(0, 300)}`);
      return `One line, total $${o.total}. My order reads: "${o.text.slice(0, 240)}"`;
    });

    await jr.step("PATRON", "tapping that line in my order reopens the dish — and it remembers "
                          + "the 2 I chose", async id => {
      const line = p.locator('[data-testid^="cart-item-"]').first();
      if (!(await line.count()))
        throw new Error("there is nothing in my order I can tap to change it");
      await line.click({ timeout: 15000 }).catch(async () => { await line.evaluate(el => el.click()); });
      await p.waitForTimeout(DWELL);
      if (!(await sheet(p).count()))
        throw new Error("tapping the line in my order opened no dish sheet — there is no edit here");
      ev.onEdit = { qty: await sheetQty(p), add: await addLabel(p) };
      await look(p, jr, "diner", id, "reopened from my order");
      if (ev.onEdit.qty !== "2")
        throw new Error(`it shows ${ev.onEdit.qty}, not the 2 already in my order`);
      return `It reopened already showing ${ev.onEdit.qty}, and the button now reads `
           + `"${ev.onEdit.add}".`;
    });

    await jr.step("PATRON", "I change NOTHING and press that button — so my order should still "
                          + "be two salads", async id => {
      await sheet(p).locator('[data-testid="composition-add"]').first().click({ timeout: 20000 });
      await p.waitForTimeout(DWELL);
      const cart = await openCart(p);
      if (!cart) throw new Error("my order would not open after I confirmed");
      await expect.poll(async () => (await readOrder(cart)).lines, { timeout: 15000 }).toBeGreaterThan(0);
      await p.waitForTimeout(750);
      const o = await readOrder(cart);
      ev.afterConfirmUnchanged = o;
      await look(p, jr, "diner", id, "order after confirming unchanged");
      jr.note(`> **The moment that matters:** my order held **2** salads. I reopened the dish `
            + `from that line — it showed **${ev.onEdit.qty}** and offered `
            + `**"${ev.onEdit.add}"**. I changed nothing and pressed it. My order now holds a `
            + `total of **$${o.total}**, which is **${o.total ? (o.total / UNIT).toFixed(0) : "?"}** `
            + `salads.`);
      if (o.lines !== 1)
        throw new Error(`my order now shows ${o.lines} separate lines: ${o.text.slice(0, 300)}`);
      if (o.total !== +(UNIT * 2).toFixed(2))
        throw new Error(`I changed nothing, yet my order went from $${(UNIT * 2).toFixed(2)} to `
                      + `$${o.total} — the sheet showed me the 2 I already had, then added `
                      + `another 2 on top when I confirmed it. My order reads: `
                      + `"${o.text.slice(0, 260)}"`);
      return `Still one line, still $${o.total}. Confirming an unchanged edit changed nothing.`;
    });

    verdict = "WALKED";
    why = "Steps 3-6 all held: two went in as one line at $17.98, tapping that line reopened "
        + "the dish pre-filled with 2, and confirming it left the order exactly as it was.";
  } catch (e) {
    verdict = "STOPPED"; why = String(e.message || e).split("\n")[0].slice(0, 400);
  } finally {
    try {
      const states = [];
      for (const t of [TABLE, ...STRAY]) states.push(`${t}: ${await emptyTable(p, RID, t)}`);
      cleanup = `emptied the unplaced orders I left behind — ${states.join(" · ")} `
              + `(no order was ever sent to the kitchen)`;
      jr.note(`> **Cleanup:** ${cleanup}`);
      await look(p, jr, "diner", "99", "tables put back");
    } catch (e) { cleanup = `cleanup failed: ${String(e.message || e).slice(0, 120)}`; }
    fs.writeFileSync(`${jr.dir}/evidence.json`, JSON.stringify(ev, null, 1));
    jr.finish(verdict, why);
    await closeRole(c, jr, "diner");
  }
});
