/* WITNESS · mk-delivery-address  (W-014 micro-kitchen-delivery-address)
 *
 * WHY THIS WALK EXISTS
 * --------------------
 * This is one of only two journeys the board itself reports as not passing. Its own steps:
 *
 *   mkda-01-both-methods-offered      PASS
 *   mkda-02-provision-dish            PASS
 *   mkda-03-method-pills-render       PASS
 *   mkda-04-address-field-reachable   PASS
 *   mkda-05-order-carries-address     FAIL
 *   mkda-06-address-on-dashboard      FAIL
 *
 * A red canary still needs confirming from outside: "the canary says it is broken" and "a
 * person finds it broken" are different claims, and only the second one is mine to make.
 *
 * WHAT I CAN AND CANNOT REACH
 * Step 05 needs a placed order, and ordering at a micro kitchen stops at a signup gate that
 * needs a confirmed DINER-pool account this container does not have. So I do NOT walk 05.
 * Step 06 is OWNER-side - "does a delivery order show its address on the dashboard" - and
 * that I CAN read, against orders the canary itself has already placed. That is the half of
 * the red I can honestly speak to.
 *
 * NO MUTATION. Nothing ordered, confirmed, cancelled or saved.
 */
import { test } from "@playwright/test";
import { journal, look, arriveAsDiner, signInOwner, account, closeRole, roleContext,
         deskContext, PATRON, OWNER, DWELL } from "../lib-witness.mjs";
import fs from "node:fs";

const RID = JSON.parse(fs.readFileSync("/workspace/uat/runs/wave0/fixtures.json","utf8"))
              ["micro-kitchen-delivery"];
const TABLE = 10461;
const ADDRESSY = /address|street|postcode|post code|zip|city|deliver to|line ?1/i;

async function answerDietary(page, ms = 12000) {
  const g = page.locator('[data-testid="m2-dietary-gate"]');
  try { await g.first().waitFor({ state: "visible", timeout: ms }); } catch { return false; }
  await page.locator('[data-testid="m2-dietary-gate-no"]').first()
            .evaluate(el => el.click()).catch(() => {});
  await g.first().waitFor({ state: "hidden", timeout: 8000 }).catch(() => {});
  return true;
}

test("mk-delivery-address", async ({ browser }) => {
  const jr = journal("mk-delivery-address");
  jr.target({ app: "patron + owner", patron: PATRON, owner: OWNER, restaurantId: RID,
              table: TABLE, fixture: "micro-kitchen-delivery" });
  jr.note("_The board reports this journey NOT passing on two steps. This walk confirms the "
        + "half of that a person can actually reach._");
  jr.note("**No mutation:** nothing ordered, confirmed, cancelled or saved.");

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

  try {
    await jr.step("PATRON", "I arrive at the micro kitchen and it offers me a way to get food",
      async id => {
        await arriveAsDiner(p, RID, TABLE, "mosaic_v2").catch(() => {});
        await p.waitForTimeout(DWELL);
        await answerDietary(p);
        const t = await look(p, jr, "patron", id, "arrival");
        const tiles = await p.locator('[data-testid^="m2-tile-"]').count();
        if (!tiles) throw new Error(`Nothing on the menu to order. Screen: ${t.slice(0,180)}`);
        return `${tiles} dish(es) I could tap. ${t.slice(0,140)}`;
      });

    await jr.step("PATRON", "I put something in my basket and go to order", 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 })
             .catch(() => {});
      await p.waitForTimeout(DWELL);
      await p.locator('[data-testid="m2-dock-trigger"]').first()
             .evaluate(el => el.click()).catch(() => {});
      await p.waitForTimeout(1500);
      await p.locator('[data-testid="m2-dock-cart"]').first()
             .evaluate(el => el.click()).catch(() => {});
      await p.waitForTimeout(DWELL);
      await answerDietary(p, 3000);
      const t = await look(p, jr, "patron", id, "the basket");
      const n = await p.locator('[data-testid="m2-dock-cart-count"]').first()
                       .innerText().catch(() => "");
      if (!n.trim()) throw new Error(`Nothing landed in the basket. Screen: ${t.slice(0,180)}`);
      return `Basket shows ${n.trim()}. ${t.slice(0,160)}`;
    });

    await jr.soft("PATRON", "Now it should ask me pickup or delivery", async id => {
      const t = await look(p, jr, "patron", id, "how do you want it");
      const ids = await p.evaluate(() =>
        [...document.querySelectorAll("[data-testid]")].map(e => e.getAttribute("data-testid"))
          .filter(x => /pickup|collect|delivery|deliver|method|fulfil|window|slot/i.test(x)).sort());
      const words = /pickup|collect|delivery/i.test(t);
      jr.note(`> **Method controls in the basket:** ${ids.length ? "`"+ids.join("`, `")+"`" : "_none by testid_"}`
            + ` · page text mentions pickup/delivery: **${words}**`);
      if (!ids.length && !words)
        throw new Error(`With a full basket, still neither a pickup nor a delivery choice `
                      + `anywhere. Screen: ${t.slice(0,220)}`);
      return `Offered: ${ids.join(", ") || "(named only in the page text)"}`;
    });

    await jr.soft("PATRON", "I can reach somewhere to tell it WHERE to deliver", async id => {
      const t = await look(p, jr, "patron", id, "where do I put my address");
      const ids = await p.evaluate(rx =>
        [...document.querySelectorAll("[data-testid]")].map(e => e.getAttribute("data-testid"))
          .filter(x => new RegExp(rx, "i").test(x)).sort(), ADDRESSY.source);
      const fields = await p.evaluate(rx => [...document.querySelectorAll("input,textarea")]
        .map(e => (e.getAttribute("placeholder")||e.getAttribute("name")||
                   e.getAttribute("aria-label")||"").trim())
        .filter(v => v && new RegExp(rx,"i").test(v)), ADDRESSY.source);
      if (!ids.length && !fields.length)
        throw new Error(`No address field and no address control is reachable from here. `
                      + `Screen: ${t.slice(0,220)}`);
      jr.note(`> **Address controls:** \`${ids.join("`, `")}\` · fields: ${fields.join(" | ")}`);
      return `I can reach: ${[...ids, ...fields].join(", ")}`;
    });

    await jr.soft("PATRON", "and where a person is stopped from finishing", async id => {
      const btn = p.locator('[data-testid="order-now-btn"]').first();
      if (!(await btn.count()))
        throw new Error("There is no Place order button on this screen at all.");
      await btn.click({ timeout: 20000 });
      await p.waitForTimeout(DWELL);
      const t = await look(p, jr, "patron", id, "stopped here");
      const gate = await p.locator('[data-testid="signup-gate"]').count();
      return gate
        ? `Pressing Place order stops me at the signup gate — a micro kitchen needs to know `
        + `who to hand the bag to. Going past it needs a confirmed diner account this `
        + `container does not hold, so I do not walk the "order carries the address" step.`
        : `It did not stop me at a gate. Screen: ${t.slice(0,220)}`;
    });

    /* ---- the owner half of the red: does a delivery order show its address? ---------- */
    await jr.step("OWNER", "As the owner, I look at the orders this kitchen has taken",
      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 * 2);
        await o.keyboard.press("Escape").catch(() => {});   // the restaurant switcher opens over the list
        await o.waitForTimeout(1500);
        const t = await look(o, jr, "owner", id, "the orders dashboard");
        /* PROVE ORDERS EXIST BEFORE ASKING WHETHER THEY CARRY AN ADDRESS.
           My first run read the switcher dropdown, found no address in it, and was about to
           report that as a finding - but "orders with no address" and "no orders at all" are
           indistinguishable without this count. Absence proves nothing until presence is
           established. */
        /* Count ORDERS, not chrome. The filter bar renders the words All / Pending /
           Confirmed / Preparing / Ready / Delivered, so a naive status-word count returns
           exactly 6 on an EMPTY dashboard - my previous version read that as "orders exist".
           Count only order-shaped things: a table reference, or a price. */
        const stats = await o.evaluate(() => {
          const m = document.querySelector("main") || document.body;
          const txt = m.innerText;
          const chipBar = /All\s+Pending\s+Confirmed\s+Preparing\s+Ready\s+Delivered/i.test(txt);
          return { prices: (txt.match(/\$\d/g) || []).length,
                   tables: (txt.match(/Table\s*#?\s*\d+/gi) || []).length,
                   emptyish: /no orders|nothing to show|all caught up/i.test(txt),
                   chipBar };
        });
        jr.note(`> **Order-shaped things on this dashboard:** ${stats.prices} price(s), `
              + `${stats.tables} table reference(s). Filter chip bar present: ${stats.chipBar} `
              + `(its six status words are chrome, not orders).`);
        if (stats.emptyish || (!stats.tables && stats.prices < 2))
          throw new Error(`I cannot establish that this kitchen has any orders on its `
                        + `dashboard (${stats.prices} price, ${stats.tables} table refs), so I `
                        + `will NOT claim anything about whether an order carries an address. `
                        + `That is a gap in my evidence, not a finding.`);
        return `The dashboard is showing real orders: ${stats.prices} price(s), `
             + `${stats.states} status word(s). ${t.slice(0,180)}`;
      });

    await jr.step("OWNER", "I open the order to see where it should go", async id => {
      /* The list row is a SUMMARY and the pane beside it says "Select an order to view
         details". Judging "no address" off the collapsed row would be reading the summary
         and calling it the record. Open it first. Clicking a row only VIEWS - it confirms,
         cancels and serves nothing. */
      const before = await o.evaluate(() =>
        (document.querySelector("main")||document.body).innerText.length);
      /* Target the row by its ORDER ID, which is the one string unique to a real order, and
         click the deepest element carrying it so the handler is not on an ancestor I miss.
         My first attempt matched a generic "Table #" container and clicked a wrapper that
         has no handler - the pane never opened, and the walk correctly refused to conclude. */
      await o.evaluate(() => {
        const m = document.querySelector("main") || document.body;
        const rx = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{4}-?[0-9a-f]{6,}/i;
        const hits = [...m.querySelectorAll("*")].filter(e => rx.test(e.textContent || ""));
        const deepest = hits[hits.length - 1];
        for (let el = deepest; el && el !== m; el = el.parentElement) {
          el.click();
          if (!/select an order to view details/i.test(m.innerText)) return;
        }
      });
      await o.waitForTimeout(DWELL);
      const t = await look(o, jr, "owner", id, "the order, opened");
      const after = await o.evaluate(() =>
        (document.querySelector("main")||document.body).innerText.length);
      const stillPrompt = /select an order to view details/i.test(t);
      if (stillPrompt && after <= before)
        throw new Error("I could not get the order to open - the pane still says 'Select an "
                      + "order to view details', so I will NOT claim anything about what it "
                      + "does or does not contain.");
      return `The order is open (detail region grew ${before} -> ${after} chars). `
           + `${t.slice(0,260)}`;
    });

    await jr.step("OWNER", "Does the open order tell me where to deliver it?", async id => {
      const t = await look(o, jr, "owner", id, "looking for the address");
      const main = await o.evaluate(() =>
        (document.querySelector("main")||document.body).innerText.replace(/\s+/g," ").trim());
      const hasWord = ADDRESSY.test(main);
      const looksLikeStreet = /\b\d+[a-z]?\s+[A-Z][a-z]+\s+(St|Street|Rd|Road|Ave|Avenue|Ln|Lane|Dr|Drive|Way|Close|Court|Ct)\b/.test(main);
      const table = (main.match(/Table\s*#?\s*\d+/i) || [])[0] || "";
      jr.note(`> **In the opened order:** address word present: **${hasWord}** · `
            + `something shaped like a street address: **${looksLikeStreet}** · `
            + `location shown as: \`${table || "nothing"}\``);
      if (looksLikeStreet) return `Yes — the open order shows a delivery address.`;
      return `No. With the order open, there is nothing shaped like a delivery address`
           + `${hasWord ? ' (the word appears; no actual address does)' : ''}. The only `
           + `location on it is ${table ? `"${table}"` : "nothing at all"} — a TABLE number, `
           + `on a kitchen whose whole point is that there are no tables. Region reads: `
           + `${main.slice(0,240)}`;
    });

    verdict = "WALKED";
    why = "Confirmed the reachable half of a journey the board reports as not passing.";
  } catch (e) {
    verdict = "STOPPED"; why = String(e.message||e).split("\n")[0].slice(0,300); throw e;
  } finally {
    jr.finish(verdict, why);
    await closeRole(pc, jr, "patron"); await closeRole(oc, jr, "owner");
  }
});
