/* WITNESS · menu-owner-hide  (W-056 mosaic_v2-owner-hide)
 *
 * THE PROMISE: an owner hides a dish; the diner's tile disappears while they are sitting
 * there, the dish already in their cart is called out as gone, and they cannot order it.
 *
 * HOW I DRIVE IT: I am two people at once — an owner on a desktop and a diner on an iPhone
 * at table 9934 — and I use the owner's own Food Items screen, not an API.
 *
 * MUTATION + CLEANUP: I flip exactly ONE dish's visibility off and back on again. The dish
 * is chosen to be one already on the diner's screen. The final step restores it and reads
 * the row back; if it cannot restore, the walk says so loudly.
 */
import { test } from "@playwright/test";
import { journal, look, arriveAsDiner, openCourse, roleContext, deskContext, closeRole,
         signInOwner, account, OWNER, PATRON, DWELL } from "../lib-witness.mjs";
import fs from "node:fs";

const RID = "28ae37ab-706c-4e20-83d7-52f5d2dff747";   // Canary Mosaic — dev
const TABLE = 9934;

/* The tile itself renders an image; the dish NAME lives in the sibling `m2-text-<id>` layer.
   Reading innerText off the tile gives you the single letter of the photo placeholder — which
   is exactly the mistake that stopped this walk the first time. */
const tiles = page => page.evaluate(() =>
  [...document.querySelectorAll('[data-testid^="m2-tile-"]')].map(e => {
    const id = e.getAttribute("data-testid").replace("m2-tile-", "");
    const txt = document.querySelector(`[data-testid="m2-text-${id}"]`);
    const name = ((txt && txt.innerText) || e.getAttribute("aria-label") || e.innerText || "")
      .replace(/\s+/g, " ").trim();
    return { tid: `m2-tile-${id}`, id, text: name.slice(0, 80) };
  }));

const rows = page => page.evaluate(() =>
  [...document.querySelectorAll('[data-testid^="food-library-visibility-toggle-"]')].map(t => {
    const r = t.closest("tr") || t.parentElement?.parentElement;
    return { tid: t.getAttribute("data-testid"),
             row: (r?.innerText || "").replace(/\s+/g, " ").trim().slice(0, 140) };
  }));


/* Switching course the way a thumb does: open the dock, tap the course, let the dock close.
   `openCourse()` in the library dispatches the click programmatically, which leaves the
   dock's full-screen scrim over the menu — and then nothing on the menu can be tapped. */
async function goToCourse(page, course, jr) {
  const trig = page.locator('[data-testid="m2-dock-trigger"]').first();
  if (await trig.count()) { await trig.click({ timeout: 15000 }).catch(() => {}); await page.waitForTimeout(1800); }
  const btn = page.locator(`[data-testid="m2-section-${course}"]`).first();
  if (!(await btn.count())) return { ok: false, course };
  let programmatic = false;
  try { await btn.click({ timeout: 12000 }); }
  catch { programmatic = true; await btn.evaluate(el => el.click()); }
  await page.waitForTimeout(6000);
  let scrimUp = await page.locator('[data-testid="m2-dock-scrim"]').first().isVisible().catch(() => false);
  if (scrimUp) {
    await page.keyboard.press("Escape").catch(() => {});
    await page.waitForTimeout(1500);
    scrimUp = await page.locator('[data-testid="m2-dock-scrim"]').first().isVisible().catch(() => false);
  }
  const tiles = await page.locator('[data-testid^="m2-tile-"]').count();
  if (jr) jr.note(`> **Getting to ${course}:** tapped the dock, `
    + `${programmatic ? "the course button had to be clicked programmatically" : "tapped the course"}; `
    + `the dock scrim ${scrimUp ? "**is still over the menu**" : "cleared"}; ${tiles} tiles.`);
  return { ok: true, course, tiles, scrimUp, programmatic };
}

test("menu-owner-hide", async ({ browser }) => {
  test.setTimeout(28 * 60_000);
  const jr = journal("menu-owner-hide");
  jr.target({ ownerApp: OWNER, patronApp: PATRON, restaurantId: RID, fixture: "canary-mosaic",
              table: TABLE, route: "/owner/food-items/ + /menu (mosaic_v2)" });
  jr.note("**Two people at once:** an owner on a desktop and a diner on an iPhone at table 9934.");
  jr.note("**Mutation:** one dish's visibility is switched off and switched back on. Nothing else.");

  const oc = await deskContext(browser, jr, "owner");
  const pc = await roleContext(browser, jr, "patron");
  const owner = await oc.newPage(), diner = await pc.newPage();
  let verdict = "INCOMPLETE", why = "", target = null, sibling = null, restored = "NOT ATTEMPTED";
  const state = {};

  try {
    await jr.step("PATRON", "I sit down and look at the food", async id => {
      await arriveAsDiner(diner, RID, TABLE, "mosaic_v2");
      let r = await goToCourse(diner, "Entrees", jr);
      if (!r.ok || !r.tiles) r = await goToCourse(diner, "Appetizers", jr);
      const t = await look(diner, jr, "patron", id, "the food course");
      state.before = await tiles(diner);
      if (state.before.length < 2)
        throw new Error(`Only ${state.before.length} dish tile(s) on ${r.course}; I need a target and a sibling.`);
      jr.note(`> **On my screen (${r.course}):** `
            + state.before.map(x => x.text.split(" ")[0]).join(" · "));
      return `${r.course}: ${state.before.length} tiles — ${state.before.map(x=>x.text.slice(0,22)).join(" | ")}`;
    });

    await jr.step("OWNER", "I open my food library and find one of those dishes", async id => {
      const a = account("canaryOwner");
      await signInOwner(owner, a.email, a.password, RID);
      await owner.goto(`${OWNER}/owner/food-items/?restaurantId=${RID}`, { waitUntil: "domcontentloaded" });
      await owner.waitForTimeout(DWELL * 2);
      const t = await look(owner, jr, "owner", id, "my food library");
      const rs = await rows(owner);
      if (!rs.length) throw new Error(`No dish rows on the food library. Screen: ${t.slice(0,200)}`);
      // Match a diner tile to an owner row by the dish name shown on the tile.
      for (const tile of state.before) {
        const name = tile.text.split("\n")[0].replace(/\$.*$/, "").trim();
        const key = name.split(" ").slice(0, 3).join(" ");
        const hit = rs.find(r => key.length > 4 && r.row.toLowerCase().includes(key.toLowerCase()));
        if (hit && !target) { target = { ...hit, name: key, tile }; continue; }
      }
      // The sibling only has to be a tile I can watch stay put — it needs no owner row.
      if (target) {
        const other = state.before.find(t => t.tid !== target.tile.tid);
        if (other) sibling = { name: other.text.slice(0, 28), tile: other };
      }
      if (!target) throw new Error(
        `I could not match any dish on the diner's screen to a row in my own food library. `
        + `Diner sees: ${state.before.map(x=>x.text.slice(0,24)).join(" | ")}. `
        + `Library rows: ${rs.slice(0,6).map(r=>r.row.slice(0,30)).join(" | ")}`);
      jr.note(`> **The dish I will hide:** \`${target.name}\` · **the sibling I will watch stay:** `
            + `\`${sibling ? sibling.name : "(none matched — I will watch the tile count instead)"}\``);
      return `${rs.length} rows. Target "${target.name}" (${target.tid}). Its row reads: ${target.row.slice(0,100)}`;
    });

    await jr.step("PATRON", "I put that dish in my cart", async id => {
      const tile = diner.locator(`[data-testid="${target.tile.tid}"]`).first();
      let blocked = "";
      try { await tile.click({ timeout: 20000 }); }
      catch (e) {
        blocked = String(e.message || e).split("\n").find(l => /intercept/i.test(l)) || "tap timed out";
        jr.note(`> ⚠️ **Tapping the dish did not work first time:** \`${blocked.trim().slice(0,140)}\``);
        await tile.evaluate(el => el.click());
      }
      state.tileTapBlocked = blocked;
      await diner.waitForTimeout(3500);
      const sheet = diner.locator('[data-testid="m2-composition"]').first();
      await sheet.waitFor({ state: "visible", timeout: 25000 }).catch(() => {});
      if (!(await sheet.count()))
        throw new Error("I tapped the dish and nothing opened — no composition sheet.");
      const add = sheet.locator('[data-testid="composition-add"]').first();
      if (!(await add.count()))
        throw new Error("The dish sheet opened but carries no Add control I can press.");
      await add.click({ timeout: 25000 });
      const t = await look(diner, jr, "patron", id, "in my cart");
      state.cartText = (t.match(/\d+\s+ITEMS?/i) || [""])[0];
      return `Cart now reads "${state.cartText || "(no count shown)"}".`;
    });

    await jr.step("OWNER", "I hide that dish", async id => {
      await owner.locator(`[data-testid="${target.tid}"]`).click({ timeout: 25000 });
      await owner.waitForTimeout(4000);
      const t = await look(owner, jr, "owner", id, "dish hidden");
      const all = await rows(owner);
      const now = all.find(r => r.tid === target.tid);
      if (!now) throw new Error(
        "STOP — after clicking the visibility control the dish's ROW is gone from my food "
        + "library entirely. I will not click anything else on this screen.");
      state.ownerRowAfterHide = now?.row || "";
      if (!/hidden/i.test(state.ownerRowAfterHide))
        jr.note(`> ⚠️ After clicking, my own row still reads: \`${state.ownerRowAfterHide.slice(0,110)}\``);
      return `My row now reads: ${state.ownerRowAfterHide.slice(0,120)}`;
    });

    await jr.step("PATRON", "Without touching anything, the dish leaves my screen", async id => {
      const gone = async () => !(await diner.locator(`[data-testid="${target.tile.tid}"]`).count());
      let realtime = false;
      for (let i = 0; i < 12 && !realtime; i++) { realtime = await gone(); if (!realtime) await diner.waitForTimeout(5000); }
      const t = await look(diner, jr, "patron", id, realtime ? "tile gone by itself" : "tile still there");
      state.after = await tiles(diner);
      state.realtime = realtime;
      if (!realtime) {
        await diner.reload({ waitUntil: "domcontentloaded" });
        await diner.waitForTimeout(DWELL + 5000);
        let r = await goToCourse(diner, "Entrees", jr);
        if (!r.ok || !r.tiles) r = await goToCourse(diner, "Appetizers", jr);
        await look(diner, jr, "patron", id + "b", "after I reload");
        state.afterReload = await tiles(diner);
        state.goneAfterReload = !state.afterReload.some(x => x.tid === target.tile.tid);
      }
      jr.note(`> **Real-time?** ${realtime ? "YES — it vanished while I sat there (within 60s)."
             : `NO — after 60s it was still on my screen. After a manual reload it was `
               + `${state.goneAfterReload ? "gone" : "STILL THERE"}.`}`);
      const sibStill = sibling ? state.after.some(x => x.tid === sibling.tile.tid) : null;
      if (sibling) jr.note(`> **Sibling \`${sibling.name}\` still on screen:** ${sibStill}`);
      return realtime
        ? `It disappeared on its own. ${state.before.length} tiles -> ${state.after.length}.`
        : `It did NOT disappear on its own in 60 seconds. After a reload: `
          + `${state.goneAfterReload ? "gone" : "still there"}.`;
    });

    await jr.soft("PATRON", "My cart tells me the dish is no longer available", async id => {
      const cart = diner.locator('[data-testid="m2-dock-cart"], [data-testid*="cart"]').first();
      if (await cart.count()) { await cart.click({ timeout: 20000 }).catch(()=>{}); await diner.waitForTimeout(4000); }
      const t = await look(diner, jr, "patron", id, "my cart after the dish was hidden");
      const notice = /no longer available|unavailable|removed|sold out|not available/i.test(t);
      const place = await diner.locator('[data-testid*="place-order"]').first();
      const hasPlace = await place.count();
      const disabled = hasPlace ? await place.isDisabled().catch(() => null) : null;
      jr.note(`> **Cart says the dish is gone:** ${notice} · **Place Order present:** ${!!hasPlace}`
            + ` · **disabled:** ${disabled}`);
      state.notice = notice; state.place = { hasPlace: !!hasPlace, disabled };
      if (!notice) throw new Error(
        `My cart shows no notice that the dish went away. What it says: ${t.slice(0,240)}`);
      return `The cart tells me it is gone. Place Order present=${!!hasPlace} disabled=${disabled}.`;
    });

    await jr.step("OWNER", "I put it back the way I found it", async id => {
      await owner.goto(`${OWNER}/owner/food-items/?restaurantId=${RID}`, { waitUntil: "domcontentloaded" });
      await owner.waitForTimeout(DWELL * 2);
      await owner.locator(`[data-testid="${target.tid}"]`).click({ timeout: 25000 });
      await owner.waitForTimeout(4000);
      const t = await look(owner, jr, "owner", id, "restored");
      const now = (await rows(owner)).find(r => r.tid === target.tid);
      restored = /visible/i.test(now?.row || "") && !/hidden/i.test(now?.row || "")
        ? "RESTORED — the row reads Visible again" : `UNCERTAIN — row now reads: ${now?.row?.slice(0,110)}`;
      jr.note(`> **Cleanup:** ${restored}`);
      return restored;
    });

    verdict = "WALKED"; why = "Hid a dish as the owner and watched what the diner saw.";
  } catch (e) {
    verdict = "STOPPED"; why = String(e.message||e).split("\n")[0].slice(0,300);
  } finally {
    fs.writeFileSync(`${jr.dir}/state.json`, JSON.stringify({ target, sibling, restored, ...state }, null, 1));
    jr.note(`**Cleanup outcome:** ${restored}`);
    jr.finish(verdict, why);
    await closeRole(pc, jr, "patron"); await closeRole(oc, jr, "owner");
  }
});
