import { test, expect } from "@playwright/test";
import fs from "node:fs";
import { journal, look, arriveAsDiner, signInWaiter, roleContext, closeRole,
         account, table, PATRON, WAITER, DWELL } from "../lib-witness.mjs";

// The journey's OWN fixture, not the house one.
const RID   = "7766d206-244c-576d-be4b-f5e746bf9832";   // Canary Mosaic V2 Electronic Pay - dev
const T     = table(10094);
const SLUG  = "w043-electronic-pay-full";

const MONEY = /\$\s?\d[\d,]*(?:\.\d{2})?/g;
const money = (s) => Array.from(new Set((s || "").match(MONEY) || []));

// Everything the two phones ask the network for. Keys are public by design (pk_);
// anything secret-shaped is recorded by PREFIX only.
const wire = [];
function watch(page, who) {
  page.on("request", r => {
    const u = r.url();
    if (/stripe|pay|settle|checkout|intent|charge|bill|order/i.test(u))
      wire.push({ who, m: r.method(), u: u.slice(0, 240) });
  });
  page.on("response", async r => {
    const u = r.url();
    if (/\/(payments?|settle|checkout|payment-intent|charge)/i.test(u))
      wire.push({ who, status: r.status(), u: u.slice(0, 240) });
  });
}

async function openServiceBubble(p) {
  const btn = p.getByTestId("layout-service-button");
  if (await btn.count()) { await btn.first().evaluate(el => el.click()); }
  else { await p.evaluate(() => window.dispatchEvent(new CustomEvent("qrate:open-service-bubble"))); }
  await p.waitForTimeout(2500);
  if (!(await p.getByTestId("service-bubble").count())) {
    await p.evaluate(() => window.dispatchEvent(new CustomEvent("qrate:open-service-bubble")));
    await p.waitForTimeout(2500);
  }
}
const bubbleText = async (p) => {
  const b = p.getByTestId("service-bubble");
  return (await b.count()) ? (await b.first().innerText()).replace(/\s+/g, " ").trim() : "";
};
const checkText = async (p) => {
  const c = p.getByTestId("pay-your-check");
  return (await c.count()) ? (await c.first().innerText()).replace(/\s+/g, " ").trim() : "";
};
const ids = (p) => p.evaluate(() =>
  Array.from(document.querySelectorAll("[data-testid]")).map(e => e.getAttribute("data-testid")).sort());


test(SLUG, async ({ browser }) => {
  const jr = journal(SLUG);
  jr.target({ app: "patron (mosaic_v2) + waiter", baseUrl: PATRON, restaurantId: RID, table: T,
              substitution: "waiter board driven with the canaryOwner login - there is no waiter account scoped to this fixture" });
  jr.note("**The promise (board, 11 steps):** two diners join and order, a server serves them, diner 1 reaches the pay-how fork, **is shown the check maths**, **is given a Stripe card element**, **completes a charge**, and afterwards **only the unpaid part remains**. My previous walk stopped at the fork. This one goes past it.");

  const cP1 = await roleContext(browser, jr, "diner-1");
  const cP2 = await roleContext(browser, jr, "diner-2");
  const p1 = await cP1.newPage(); watch(p1, "diner-1");
  const p2 = await cP2.newPage(); watch(p2, "diner-2");

  let verdict = "INCOMPLETE", why = "", leftBehind = "nothing", stripeMode = "unknown";
  const seen = {};

  try {
    await jr.soft("DINER 1", "I scan the code and the menu comes up", async id => {
      const gates = await arriveAsDiner(p1, RID, T, "mosaic_v2");
      const t = await look(p1, jr, "diner-1", id, "menu on arrival");
      seen.arrivePrices = money(t);
      if (!/Canary|\$/.test(t)) throw new Error("no menu rendered");
      return `gates ${JSON.stringify(gates)} | prices on screen ${JSON.stringify(seen.arrivePrices.slice(0,6))} | ${t.slice(0,220)}`;
    });

    await jr.soft("DINER 2", "my friend scans the same code and lands on the same table", async id => {
      await arriveAsDiner(p2, RID, T, "mosaic_v2");
      const t = await look(p2, jr, "diner-2", id, "menu on arrival");
      if (!/\$/.test(t)) throw new Error("no menu rendered for diner 2");
      return t.slice(0, 220);
    });

    await jr.soft("DINER 1", "I put a drink in my basket", async id => {
      const adds = p1.locator('[data-testid^="m2-add-"]');
      const n = await adds.count();
      if (!n) throw new Error("nothing on the menu can be added");
      seen.p1dish = await adds.first().getAttribute("data-testid");
      await adds.first().evaluate(el => el.click());
      await p1.waitForTimeout(2500);
      const t = await look(p1, jr, "diner-1", id, "after adding my drink");
      return `${n} addable tiles; I added ${seen.p1dish} | ${t.slice(0,180)}`;
    });

    await jr.soft("DINER 2", "she adds something different, so the check has two names on it", async id => {
      const adds = p2.locator('[data-testid^="m2-add-"]');
      const n = await adds.count();
      if (n < 2) throw new Error("fewer than two distinct dishes to choose from");
      seen.p2dish = await adds.nth(1).getAttribute("data-testid");
      await adds.nth(1).evaluate(el => el.click());
      await p2.waitForTimeout(2500);
      const t = await look(p2, jr, "diner-2", id, "after adding her drink");
      return `she added ${seen.p2dish} | ${t.slice(0,180)}`;
    });

    await jr.soft("DINER 1", "I open my basket and it tells me what this costs", async id => {
      const open = p1.getByTestId("m2-view-order");
      if (await open.count()) await open.first().evaluate(el => el.click());
      else await p1.getByTestId("m2-dock-cart").first().evaluate(el => el.click());
      await p1.waitForTimeout(2500);
      const t = await look(p1, jr, "diner-1", id, "my basket");
      const sheet = p1.getByTestId("cart-sheet");
      const inSheet = (await sheet.count()) ? (await sheet.first().innerText()).replace(/\s+/g," ").trim() : t;
      seen.cartMoney = money(inSheet);
      seen.cartTotal = (await p1.getByTestId("cart-footer-total").count())
        ? (await p1.getByTestId("cart-footer-total").first().innerText()).trim() : "(no cart-footer-total)";
      seen.cartCta = (await p1.getByTestId("cart-cta-amount").count())
        ? (await p1.getByTestId("cart-cta-amount").first().innerText()).trim() : "(no cart-cta-amount)";
      if (!seen.cartMoney.length) throw new Error("basket shows no money figure at all");
      return `IN THE SHEET: footer-total=${seen.cartTotal} cta=${seen.cartCta} figures=${JSON.stringify(seen.cartMoney)} | ${inSheet.slice(0,260)}`;
    });

    await jr.soft("DINER 1", "I send my order to the kitchen", async id => {
      const btn = p1.getByTestId("order-now-btn");
      if (!(await btn.count())) throw new Error("no order-now-btn in the basket");
      seen.orderCta = (await btn.first().innerText()).replace(/\s+/g," ").trim();
      await btn.first().evaluate(el => el.click());
      await p1.waitForTimeout(6000);
      const t = await look(p1, jr, "diner-1", id, "after placing my order");
      return `the button said "${seen.orderCta}" | ${t.slice(0,240)}`;
    });

    await jr.soft("DINER 2", "she sends hers too, so we both have something unpaid", async id => {
      const open = p2.getByTestId("m2-view-order");
      if (await open.count()) await open.first().evaluate(el => el.click());
      else await p2.getByTestId("m2-dock-cart").first().evaluate(el => el.click());
      await p2.waitForTimeout(2500);
      const btn = p2.getByTestId("order-now-btn");
      if (!(await btn.count())) throw new Error("diner 2 has no order-now-btn");
      const cta = (await btn.first().innerText()).replace(/\s+/g," ").trim();
      await btn.first().evaluate(el => el.click());
      await p2.waitForTimeout(6000);
      const t = await look(p2, jr, "diner-2", id, "her order sent");
      return `her button said "${cta}" | ${t.slice(0,240)}`;
    });

    // ---- waiter -------------------------------------------------------
    const cW = await roleContext(browser, jr, "waiter", "Desktop Chrome");
    const w = await cW.newPage(); watch(w, "waiter");
    await jr.soft("WAITER", "the server sees our table and marks the food served", async id => {
      const a = account("canaryOwner");
      await signInWaiter(w, a.email, a.password);
      await w.waitForTimeout(6000);
      const t = await look(w, jr, "waiter", id, "the server board");
      seen.waiterIds = (await ids(w)).filter(x => /tab-|table|order|serve|bill/i.test(x)).slice(0, 40);
      if (!/Active|Orders|Tables/i.test(t)) throw new Error(`waiter board did not load: ${t.slice(0,160)}`);
      return `board says: ${t.slice(0,300)} | controls ${JSON.stringify(seen.waiterIds)}`;
    });

    // ---- the settle path ---------------------------------------------
    await jr.soft("DINER 1", "I open the help menu to ask for the bill", async id => {
      await openServiceBubble(p1);
      const t = await look(p1, jr, "diner-1", id, "the help drawer");
      seen.bubbleBefore = await bubbleText(p1);
      seen.bubbleIds = (await ids(p1)).filter(x => x.startsWith("service-bubble") || x.startsWith("settlement"));
      if (!seen.bubbleIds.length) throw new Error("the help drawer never opened");
      return `DRAWER SAYS: "${seen.bubbleBefore}" | controls ${JSON.stringify(seen.bubbleIds)}`;
    });

    await jr.soft("DINER 1", "I ask for the check, and expect to be asked how I want to settle", async id => {
      const cp = p1.getByTestId("service-bubble-ready_for_bill");
      if (!(await cp.count())) throw new Error(`no ready_for_bill control; drawer offered ${JSON.stringify(seen.bubbleIds)}`);
      await cp.first().evaluate(el => el.click());
      await p1.waitForTimeout(4000);
      const t = await look(p1, jr, "diner-1", id, "after asking for the check");
      seen.settleText = await bubbleText(p1);
      seen.settleMoney = money(seen.settleText);
      seen.settleIds = (await ids(p1)).filter(x => /settlement|cover|payhow|service-/.test(x));
      return `SHEET, WORD FOR WORD: "${seen.settleText}" | money figures INSIDE the sheet: ${JSON.stringify(seen.settleMoney)} | controls ${JSON.stringify(seen.settleIds)}`;
    });

    await jr.soft("DINER 1", "I choose to pay for just my own food", async id => {
      const cover = p1.getByTestId("settlement-cover");
      if (await cover.count()) {
        await cover.first().evaluate(el => el.click());
        await p1.waitForTimeout(3000);
        await look(p1, jr, "diner-1", id, "who am I covering");
        seen.coverText = await bubbleText(p1);
      } else {
        seen.coverText = `(no settlement-cover offered - drawer went straight to ${JSON.stringify(seen.settleIds)})`;
      }
      const me = p1.getByTestId("cover-scope-just-me");
      if (!(await me.count())) throw new Error(`no "just me" choice. Sheet: "${seen.coverText}"`);
      await me.first().evaluate(el => el.click());
      await p1.waitForTimeout(4000);
      const t = await look(p1, jr, "diner-1", id, "after choosing just me");
      seen.afterScope = await bubbleText(p1);
      seen.afterScopeMoney = money(seen.afterScope);
      return `COVER SHEET: "${seen.coverText}" -> AFTER "just me": "${seen.afterScope}" | money: ${JSON.stringify(seen.afterScopeMoney)}`;
    });

    await jr.soft("DINER 1", "before I pick a payment method the app tells me what I owe", async id => {
      const t = await look(p1, jr, "diner-1", id, "the pay-how fork");
      seen.forkText = await bubbleText(p1);
      seen.forkMoney = money(seen.forkText);
      seen.forkIds = (await ids(p1)).filter(x => /payhow|settlement|cover|pyc-/.test(x));
      if (!seen.forkMoney.length)
        throw new Error(`asked HOW to pay with no amount anywhere in the sheet. Sheet: "${seen.forkText}"`);
      return `FORK: "${seen.forkText}" | money ${JSON.stringify(seen.forkMoney)}`;
    });

    await jr.soft("DINER 1", "I choose to pay by card on my own phone", async id => {
      const pay = p1.getByTestId("payhow-pay-now");
      if (!(await pay.count())) throw new Error(`no "pay now on my phone" control; fork offered ${JSON.stringify(seen.forkIds)}`);
      seen.payNowLabel = (await pay.first().innerText()).replace(/\s+/g," ").trim();
      await pay.first().evaluate(el => el.click());
      await p1.waitForTimeout(12000);
      const t = await look(p1, jr, "diner-1", id, "the check sheet");
      seen.checkIds = (await ids(p1)).filter(x => x.startsWith("pyc-") || x === "pay-your-check");
      if (!seen.checkIds.includes("pay-your-check")) throw new Error(`no check sheet appeared. Screen: ${t.slice(0,240)}`);
      return `after "${seen.payNowLabel}" the check sheet opened with ${JSON.stringify(seen.checkIds)}`;
    });

    await jr.soft("DINER 1", "the check is itemised and adds up", async id => {
      const t = await look(p1, jr, "diner-1", id, "the check itemised");
      seen.check = await checkText(p1);
      const g = async (tid) => (await p1.getByTestId(tid).count())
        ? (await p1.getByTestId(tid).first().innerText()).replace(/\s+/g," ").trim() : null;
      seen.checkLines   = await p1.getByTestId("pyc-line-item").allInnerTexts().catch(() => []);
      seen.checkCaption = await g("pyc-line-items-caption");
      seen.subtotal     = await g("pyc-subtotal");
      seen.tax          = await g("pyc-tax");
      seen.amountDue    = await g("pyc-amount-due");
      seen.total        = await g("pyc-total");
      seen.tipOptions   = { t18: await g("pyc-tip-18"), t20: await g("pyc-tip-20"), t25: await g("pyc-tip-25"), custom: await g("pyc-tip-custom") };
      if (!seen.amountDue) throw new Error(`the check sheet shows no amount due. Sheet: "${String(seen.check).slice(0,300)}"`);
      const num = (x) => Number(String(x || "").replace(/[^0-9.]/g, ""));
      seen.mathsOk = Math.abs((num(seen.subtotal) + num(seen.tax)) - num(seen.amountDue)) < 0.02;
      if (!seen.mathsOk)
        throw new Error(`subtotal ${seen.subtotal} + tax ${seen.tax} does not equal amount due ${seen.amountDue}`);
      return `CAPTION "${seen.checkCaption}" LINES ${JSON.stringify(seen.checkLines)} | subtotal ${seen.subtotal} + tax ${seen.tax} = amount due ${seen.amountDue} | tips ${JSON.stringify(seen.tipOptions)} | total ${seen.total} | I asked to cover JUST ME`;
    });

    await jr.soft("DINER 1", "I add a 20% tip and the total moves to match", async id => {
      const tip = p1.getByTestId("pyc-tip-20");
      if (!(await tip.count())) throw new Error("no 20% tip control on the check");
      const before = seen.total;
      await tip.first().evaluate(el => el.click());
      await p1.waitForTimeout(3000);
      const t = await look(p1, jr, "diner-1", id, "20 percent tip chosen");
      seen.totalAfterTip = (await p1.getByTestId("pyc-total").count())
        ? (await p1.getByTestId("pyc-total").first().innerText()).replace(/\s+/g," ").trim() : null;
      const num = (x) => Number(String(x || "").replace(/[^0-9.]/g, ""));
      seen.tipMathOk = Math.abs((num(seen.amountDue) + num(seen.tipOptions.t20)) - num(seen.totalAfterTip)) < 0.02;
      if (!seen.tipMathOk)
        throw new Error(`amount due ${seen.amountDue} + 20% tip ${seen.tipOptions.t20} does not equal total ${seen.totalAfterTip}`);
      return `total was ${before}, after tapping 20% (${seen.tipOptions.t20}) it reads ${seen.totalAfterTip}`;
    });

    await jr.soft("DINER 1", "I continue to payment and a real card form appears", async id => {
      const cont = p1.getByTestId("pyc-continue");
      if (!(await cont.count())) throw new Error("no Continue to payment control");
      await cont.first().evaluate(el => el.click());
      await p1.waitForTimeout(15000);
      const t = await look(p1, jr, "diner-1", id, "the card form");
      seen.payAfter = await checkText(p1);
      const btn = p1.getByTestId("pyc-pay-btn");
      seen.payBtn = (await btn.count()) ? {
        text: (await btn.first().innerText()).replace(/\s+/g," ").trim(),
        disabled: await btn.first().isDisabled().catch(() => null),
      } : null;
      const frames = p1.frames().filter(f => /stripe/i.test(f.url()) || /stripe/i.test(f.name() || ""));
      seen.stripeFrames = frames.map(f => ({ name: f.name(), url: f.url().replace(/(pk_(?:test|live)_)[A-Za-z0-9]+/g, "$1<redacted>").slice(0, 160) }));
      const blob = frames.map(f => f.url()).join(" ") + " " + JSON.stringify(wire);
      stripeMode = /pk_live_/.test(blob) ? "LIVE" : /pk_test_/.test(blob) ? "TEST" : "unknown";
      seen.stripeMode = stripeMode;
      if (!frames.length) throw new Error(`no Stripe frame after Continue. Sheet: "${String(seen.payAfter).slice(0,300)}"`);
      return `PAY BUTTON ${JSON.stringify(seen.payBtn)} | ${frames.length} stripe frame(s) ${JSON.stringify(seen.stripeFrames)} | MODE=${stripeMode} | sheet "${String(seen.payAfter).slice(0,240)}"`;
    });

    await jr.soft("DINER 1", "I type a card in and pay", async id => {
      if (stripeMode !== "TEST")
        throw new Error(`I will not enter a card. Stripe key mode read as ${stripeMode}; I only complete a charge against an observed pk_test_ key. The card form is on screen and the pay button reads ${JSON.stringify(seen.payBtn)}.`);
      let filled = [];
      for (const f of p1.frames()) {
        if (!/stripe/i.test(f.url())) continue;
        for (const [sel, val] of [['input[name="number"]','4242424242424242'],
                                  ['input[name="cardnumber"]','4242424242424242'],
                                  ['input[name="expiry"]','1234'],
                                  ['input[name="exp-date"]','1234'],
                                  ['input[name="cvc"]','123'],
                                  ['input[name="postalCode"]','42424'],
                                  ['input[name="postal"]','42424']]) {
          const l = f.locator(sel).first();
          if (await l.count().catch(() => 0)) { await l.fill(val).catch(() => {}); filled.push(sel); }
        }
      }
      seen.cardFieldsFilled = filled;
      await p1.waitForTimeout(2500);
      await look(p1, jr, "diner-1", id + "a", "card typed in");
      const btn = p1.getByTestId("pyc-pay-btn").first();
      seen.payBtnAfterCard = (await btn.count()) ? {
        text: (await btn.innerText()).replace(/\s+/g," ").trim(),
        disabled: await btn.isDisabled().catch(() => null) } : null;
      if (!filled.length) throw new Error("the Stripe frame exposed no field I could type a card number into");
      if (seen.payBtnAfterCard && seen.payBtnAfterCard.disabled)
        throw new Error(`card entered but the pay button is still disabled: ${JSON.stringify(seen.payBtnAfterCard)}`);
      await btn.click();
      await p1.waitForTimeout(25000);
      const t = await look(p1, jr, "diner-1", id, "after paying");
      seen.afterPay = (await checkText(p1)) || t;
      if (!/paid|thank|receipt|success|complete|settled/i.test(String(seen.afterPay)))
        throw new Error(`no confirmation after submitting the card. Screen: "${String(seen.afterPay).slice(0,300)}"`);
      return `filled ${JSON.stringify(filled)}; pay button ${JSON.stringify(seen.payBtnAfterCard)}; after paying: "${String(seen.afterPay).slice(0,400)}"`;
    });

    await jr.soft("DINER 1", "I ask for the check again and only the unpaid part is left", async id => {
      await p1.reload(); await p1.waitForTimeout(DWELL);
      await openServiceBubble(p1);
      const t = await look(p1, jr, "diner-1", id, "the check after paying");
      seen.afterCheck = (await bubbleText(p1)) || (await checkText(p1)) || t;
      seen.afterCheckMoney = money(seen.afterCheck);
      seen.afterCheckIds = (await ids(p1)).filter(x => /service-bubble|settlement|pyc-|payhow/.test(x));
      return `DRAWER NOW: "${seen.afterCheck}" | money ${JSON.stringify(seen.afterCheckMoney)} | controls ${JSON.stringify(seen.afterCheckIds)}`;
    });

    await jr.soft("DINER 2", "her phone tells her what is happening to her bill", async id => {
      await p2.reload(); await p2.waitForTimeout(DWELL);
      const t = await look(p2, jr, "diner-2", id, "her phone while I settle");
      await openServiceBubble(p2);
      const t2 = await look(p2, jr, "diner-2", id + "b", "her help drawer");
      seen.p2Bubble = await bubbleText(p2);
      if (!/paid|settl|cover|bill|check/i.test(t + " " + seen.p2Bubble))
        throw new Error(`nothing on her phone mentions the bill being settled. Screen: "${t.slice(0,200)}" drawer: "${seen.p2Bubble}"`);
      return `HER SCREEN: ${t.slice(0,220)} | HER DRAWER: "${seen.p2Bubble}"`;
    });

    verdict = "WALKED";
    why = `Drove the full electronic-pay journey on its own fixture (${RID}) at table ${T}, two diners on iPhone 14 Pro geometry over chromium. Stripe mode read as ${stripeMode}.`;
    await closeRole(cW, jr, "waiter");
  } catch (e) {
    why = String(e && e.message || e).split("\n")[0];
  } finally {
    leftBehind = `Table ${T} on ${RID}: two guests, two placed orders, one check request${stripeMode === "TEST" ? " and one test-mode card payment" : " and an un-completed pay intent"}.`;
    jr.note("### The wire, filtered\n\n```\n" + wire.slice(0, 60).map(x => `${x.who} ${x.m || x.status} ${x.u}`).join("\n") + "\n```");
    jr.note("### Everything I read off the screens\n\n```json\n" + JSON.stringify(seen, null, 1).slice(0, 6000) + "\n```");
    jr.note(`> **What I left behind:** ${leftBehind}`);
    jr.finish(verdict, why);
    await closeRole(cP1, jr, "diner-1");
    await closeRole(cP2, jr, "diner-2");
    fs.writeFileSync(`${jr.dir}/WIRE.json`, JSON.stringify(wire, null, 1));
  }
  expect(verdict, why).not.toBe("INCOMPLETE");
});
