/* WITNESS · W-027 mosaic_v2-boost-special
 *
 * WHAT THE JOURNEY PROMISES (its own 6-step list): the restaurant creates a BOOSTED dish and
 * a CHEF SPECIAL over the API, attaches them to the menu, sets the boost and the chef flag,
 * waits for propagation, and then a diner sees "Both On Hero".
 *
 * Five of the six steps are the canary creating and configuring its own throwaway dishes. The
 * sixth is the only one with a screen behind it, and it is the one that matters to a
 * restaurant: does the menu a diner is shown actually PUSH the dishes the owner promoted?
 *
 * So I ask three plain questions of the running menu:
 *   1. is there a hero — anywhere a dish is presented larger or first, as a feature?
 *   2. is any dish marked as boosted, a chef's special, featured or a top pick?
 *   3. are the canary's boosted / chef-special dishes on this menu at all?
 * I measure the tiles rather than trusting a word, because a hero is a visual promise.
 *
 * WHICH RESTAURANT: none of the canary owner's 17 restaurants is named for this journey and
 * the dishes are created at run time, so I cannot be told which fixture it drives. I walk the
 * general mosaic v2 fixture and say so.
 */
import { test } from "@playwright/test";
import { journal, look, arriveAsDiner, roleContext, closeRole, openCourse, PATRON, DWELL } from "../lib-witness.mjs";

const RID   = "28ae37ab-706c-4e20-83d7-52f5d2dff747";   // Canary Mosaic — dev
const TABLE = 9945;
const COURSES = ["Beverages","Appetizers","Entrees","Desserts"];

test("recs-hero-boost", async ({ browser }) => {
  const jr = journal("recs-hero-boost");
  jr.target({ journey: "W-027 mosaic_v2-boost-special", app: "patron (mosaic_v2)", baseUrl: PATRON,
              restaurant: "Canary Mosaic — dev", restaurantId: RID, table: TABLE });
  jr.note("**The board calls this PASSED on 6 steps.** Five are the canary creating and "
        + "configuring its own dishes over the API. The sixth — *Both On Hero* — is the only one "
        + "a person can see, and it is the whole point of boosting a dish.");

  const ctx = await roleContext(browser, jr, "patron");
  const p = await ctx.newPage();
  let verdict = "INCOMPLETE", why = "", survey = {};

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

    await jr.step("PATRON", "I look, course by course, for a dish the restaurant is pushing",
      async id => {
        for (const c of COURSES) {
          const r = await openCourse(p, c);
          const tiles = await p.evaluate(() => [...document.querySelectorAll('[data-testid^="m2-tile-"]')]
            .map(el => { const b = el.getBoundingClientRect();
                         const t = el.closest("[data-testid]")?.parentElement?.innerText || el.innerText;
                         return { area: Math.round(b.width*b.height), w: Math.round(b.width), h: Math.round(b.height),
                                  top: Math.round(b.top), text: (t||"").replace(/\s+/g," ").slice(0,60) }; }));
          const names = (await p.locator('[data-testid^="m2-text-"]').allInnerTexts().catch(()=>[]))
                          .map(x=>x.replace(/\s+/g," ").trim().slice(0,50)).filter(Boolean);
          const marks = await p.evaluate(() => [...new Set([...document.querySelectorAll("[data-testid]")]
            .map(e=>e.getAttribute("data-testid"))
            .filter(t=>/hero|feature|chef|special|boost|pick|badge|promoted/i.test(t)))]);
          const words = [...new Set(((await p.locator('[data-testid="m2-course-viewport"], [data-testid="m2-masonry"]')
                          .first().innerText().catch(()=>"")).match(/chef|special|featured|boosted|top pick|recommended|popular/gi))||[])];
          const areas = tiles.map(t=>t.area).sort((a,b)=>b-a);
          survey[c] = { tiles: r.tiles ?? tiles.length, names, marks, words,
                        biggest: areas[0]||0, smallest: areas[areas.length-1]||0,
                        sizes: [...new Set(tiles.map(t=>`${t.w}x${t.h}`))] };
          await look(p, jr, "patron", `${id}${c[0]}`, `course ${c}`);
        }
        jr.note("> **What each course actually shows me:**\n> ```\n> "
              + Object.entries(survey).map(([k,v]) =>
                  `${k}: ${v.tiles} dishes · tile sizes ${JSON.stringify(v.sizes)} · `
                + `promo words ${JSON.stringify(v.words)} · promo controls ${JSON.stringify(v.marks)}\n>     ${v.names.join(" · ")}`
                ).join("\n> ") + "\n> ```");
        const anyMark = Object.values(survey).some(v => v.words.length || v.marks.length);
        const anyHero = Object.values(survey).some(v => v.sizes.length > 1 && v.biggest > v.smallest * 1.6);
        return `a dish marked as boosted / chef's special / featured / a top pick: ${anyMark}. `
             + `A tile visibly larger than its neighbours (a hero treatment): ${anyHero}.`;
      });

    await jr.step("PATRON", "If the restaurant boosted something, I can tell which dish it is",
      async id => {
        const allNames = [...new Set(Object.values(survey).flatMap(v=>v.names))];
        const promo = allNames.filter(n=>/boost|chef|special|featured|signature/i.test(n));
        await look(p, jr, "patron", id, "the whole menu, looked over");
        jr.note(`> **Every dish name on this menu:** ${allNames.join(" · ") || "(none read)"}`);
        if (!promo.length)
          return "NO. Nothing on this menu is presented as boosted or as a chef's special — no "
               + "badge, no wording, no larger tile, and no dish named for either. As a diner I "
               + "am shown a flat, evenly-sized grid, so 'Both On Hero' has no counterpart I can "
               + "see. Either the canary's two dishes are torn down after each run, or this menu "
               + "has no hero for them to land on.";
        return `dishes that name themselves as promoted: ${promo.join(" · ")}`;
      });

    await jr.soft("PATRON", "Does a hero exist anywhere in this product, and what is on it?",
      async id => {
        // On this restaurant I found no hero at all. Before I say the product has none, I look at
        // a sibling canary restaurant - Canary Kitchen - dev - where a hero DID render during my
        // W-003 walk. If a hero exists there, then "Both On Hero" has a real surface and the
        // question becomes what is on it.
        const rid = "99e9d621-6941-4acc-8d85-50f15f3a2f2b";
        // Arrive the way a diner does - welcome swipe, dietary answer - not a bare page load.
        // A bare load did NOT render the hero; the full arrival did, so the arrival matters.
        await arriveAsDiner(p, rid, TABLE, "mosaic_v2");
        await p.waitForTimeout(6000);
        const hero = p.locator('[data-testid="m2-hero"]').first();
        const has = await hero.count();
        const slides = has ? await p.locator('[data-testid^="m2-hero-slide-"]').count() : 0;
        // read ONLY inside the hero - the menu grid shows through everything else
        const heroText = has ? (await hero.innerText().catch(()=>"")).replace(/\s+/g," ") : "";
        await look(p, jr, "patron", id, "Canary Kitchen hero");
        jr.note(`> **Canary Kitchen — dev hero:** present ${!!has}, ${slides} slide(s). It reads: `
              + `"${heroText.slice(0,240)}"`);
        if (!has) throw new Error("no hero on this restaurant either");
        return `a hero DOES exist in this product: ${slides} slide(s) on Canary Kitchen — dev, `
             + `reading "${heroText.slice(0,180)}". Canary Mosaic — dev, the restaurant I walked `
             + `for this journey, has none at all.`;
      });

    verdict = "WALKED"; why = "looked over every course for a hero and for a promoted dish";
  } catch (e) {
    verdict = "STOPPED"; why = String(e.message||e).split("\n")[0].slice(0,300);
  } finally {
    jr.note("**Cleanup:** none — I changed nothing and ordered nothing.");
    jr.finish(verdict, why);
    await closeRole(ctx, jr, "patron");
  }
});
