import { expect, test } from "@playwright/test";

import { getEvidenceAll, getFacts, getUi } from "../lib/content";
import { LOCALES } from "../lib/locales";
import { href } from "../lib/routes";

import { PAGES } from "./routes";

/**
 * MOTION, AND WHAT IT IS NOT ALLOWED TO COST.
 *
 * V3.1 added a typographic entrance to every section. The whole risk of that
 * kind of work is that a reader ends up unable to read something — a band that
 * never un-hides because a script failed, a scroll position that never fires
 * an observer, a reduced-motion user left looking at opacity zero.
 *
 * These assertions encode the three states the site has to be correct in, and
 * they check the only thing that actually matters in each: that the words are
 * on the screen.
 */

/** Walk the page the way a reader does, so every observer gets its chance. */
async function readThrough(page: import("@playwright/test").Page) {
  await page.evaluate(async () => {
    for (let y = 0; y < document.body.scrollHeight; y += 400) {
      window.scrollTo(0, y);
      await new Promise((r) => setTimeout(r, 60));
    }
    window.scrollTo(0, document.body.scrollHeight);
  });
  await page.waitForTimeout(900);
}

/** Every visible piece of text, and whether anything is still faded out. */
async function fadedText(page: import("@playwright/test").Page) {
  return page.evaluate(() =>
    [...document.querySelectorAll("h1,h2,h3,p,li,a,button")]
      .filter((el) => {
        if (!el.textContent?.trim()) return false;
        const cs = getComputedStyle(el);
        if (cs.display === "none" || cs.visibility === "hidden") return false;
        if ((el as HTMLElement).closest("[hidden]")) return false;
        return Number(cs.opacity) < 0.95;
      })
      .map((el) => `${el.tagName}.${String(el.className).slice(0, 40)}`)
      .slice(0, 10),
  );
}

test.describe("motion never hides content", () => {
  for (const locale of LOCALES) {
    test(`${locale} — every band finishes its entrance when read through`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));
      await readThrough(page);

      const states = await page.evaluate(() =>
        [...document.querySelectorAll("[data-enter]")].map((el) => el.getAttribute("data-enter")),
      );
      expect(states.length, "the page is built from sections").toBeGreaterThan(4);
      expect(
        states.filter((s) => s === "out"),
        "no band is left part-way through its entrance",
      ).toEqual([]);

      expect(await fadedText(page), "every word is legible after reading through").toEqual([]);
    });

    test(`${locale} — reduced motion is never armed at all`, async ({ browser }) => {
      const context = await browser.newContext({ reducedMotion: "reduce" });
      const page = await context.newPage();
      await page.goto(href({ key: "home" }, locale));
      await readThrough(page);

      // Not "armed and then finished" — never armed. The band a reader who
      // asked for stillness gets is the one the server sent.
      const armed = await page.locator('[data-enter="out"], [data-enter="in"]').count();
      expect(armed, "nothing is armed when motion is declined").toBe(0);
      expect(await fadedText(page), "every word is legible with reduced motion").toEqual([]);

      await context.close();
    });

    test(`${locale} — with JavaScript off nothing is armed and nothing is hidden`, async ({
      browser,
    }) => {
      const context = await browser.newContext({ javaScriptEnabled: false });
      const page = await context.newPage();
      await page.goto(href({ key: "home" }, locale));

      expect(await page.locator('[data-enter="out"]').count(), "no script, no hidden band").toBe(0);

      // The headline and every section heading are painted and readable.
      await expect(page.locator("h1")).toBeVisible();
      for (const heading of await page.locator("main h2").all()) {
        await expect(heading).toBeVisible();
      }

      await context.close();
    });
  }
});

/**
 * THE EVIDENCE LAYER.
 *
 * Public-source material is a factual claim like any other on this site, so it
 * is held to the same two rules: nothing reaches a third party before a reader
 * asks, and nothing appears that the fact register has not cleared.
 */
test.describe("public-source evidence is evidence, not promotion", () => {
  for (const page of PAGES) {
    test(`${page.name} — no third-party frame or image before activation`, async ({
      page: browserPage,
    }) => {
      await browserPage.goto(page.path);

      // The player is built on activation. Until then there is no iframe, and
      // no image is fetched from a Google domain to decorate one.
      await expect(browserPage.locator("iframe")).toHaveCount(0);
      const remote = await browserPage.evaluate(() =>
        [...document.querySelectorAll("img[src], source[srcset]")]
          .map((el) => el.getAttribute("src") ?? el.getAttribute("srcset") ?? "")
          .filter((src) => /^https?:\/\//.test(src)),
      );
      expect(remote, "nothing is loaded from a third-party host").toEqual([]);
    });
  }

  for (const locale of LOCALES) {
    test(`${locale} — every citation names its source and offers a real action`, async ({
      page,
    }) => {
      const t = (key: string) => getUi(locale)[key];
      await page.goto(href({ key: "module", module: "bar-restorant" }, locale));

      const items = page.locator("[data-source]");
      const count = await items.count();
      expect(count, "the Caffe page cites the archive").toBeGreaterThan(0);

      for (let i = 0; i < count; i++) {
        const item = items.nth(i);
        const text = (await item.innerText()).replace(/\s+/g, " ");

        // The source is stated. Nothing is passed off as the site's own.
        const source = (await item.getAttribute("data-source")) ?? "";
        const label =
          source === "youtube" ? t("evidence.sourceYoutube") : t("evidence.sourceFacebook");
        expect(text, "the citation names where it came from").toContain(label);

        // And the action says what it does, rather than naming a platform.
        const cta = await item.locator("a, button").first().innerText();
        expect(cta.trim().length, "the action is a sentence, not a logo").toBeGreaterThan(6);
        expect(cta.trim()).not.toMatch(/^(YouTube|Facebook)\s*[→↗]?$/i);
      }
    });

    test(`${locale} — a gated citation does not reach the page`, async ({ page }) => {
      const facts = getFacts();
      const gated = getEvidenceAll(locale).filter((item) => !facts[item.fact]?.verified);
      expect(gated.length, "there is something gated to check").toBeGreaterThan(0);

      // Every page a gated item is placed on, with its URL absent from it.
      for (const item of gated) {
        const target =
          item.placement === "home"
            ? href({ key: "home" }, locale)
            : item.placement === "company"
              ? href({ key: "company" }, locale)
              : item.placement === "nexys"
                ? href({ key: "nexys" }, locale)
                : href({ key: "module", module: item.placement }, locale);

        await page.goto(target);
        const html = await page.content();
        expect(html, `gated evidence "${item.id}" stays off the page`).not.toContain(item.ref);
      }
    });
  }
});

/**
 * PRODUCT MEDIA.
 *
 * The homepage leads with recordings of Nexys working rather than with a
 * timeline of what Sulsys has published. These assertions hold the three
 * things that make that defensible: nothing reaches Google before a reader
 * asks, the poster that does ship is ours and is actually served, and a video
 * is attached to the product it shows.
 */
test.describe("Nexys is shown working", () => {
  for (const locale of LOCALES) {
    test(`${locale} — the home page leads with videos, not a timeline`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));

      const cards = page.locator('[data-event="video_play"]');
      const count = await cards.count();
      expect(count, "two or three recordings, no more").toBeGreaterThanOrEqual(2);
      expect(count, "two or three recordings, no more").toBeLessThanOrEqual(3);

      // The public-trail timeline moved to the company page, where the company
      // is the subject. It must not have come back here.
      const t = (key: string) => getUi(locale)[key];
      const main = (await page.locator("main").innerText()).replace(/\s+/g, " ");
      expect(main, "the trail heading is off the home page").not.toContain(t("heading.trail"));

      // And the company page still has it, in full.
      await page.goto(href({ key: "company" }, locale));
      await expect(page.getByText(t("heading.trail")).first()).toBeVisible();
    });

    test(`${locale} — every card says what it is before it is opened`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));
      const t = (key: string) => getUi(locale)[key];

      for (const card of await page.locator('[data-event="video_play"]').all()) {
        // The stage is laid out at 16:9 whether it holds a poster or a plate,
        // so opening one can never move the page.
        const box = await card.boundingBox();
        expect(box, "the stage has a box").not.toBeNull();
        const ratio = (box?.width ?? 0) / (box?.height ?? 1);
        expect(ratio, "the stage is 16:9").toBeGreaterThan(1.7);
        expect(ratio, "the stage is 16:9").toBeLessThan(1.85);

        // A play affordance and a running time, before anything is loaded.
        const label = (await card.getAttribute("aria-label")) ?? "";
        expect(label.length, "the play control names what it opens").toBeGreaterThan(12);
      }

      // Source, module and year are all stated on the card.
      const section = page.locator("section", { has: page.locator('[data-event="video_play"]') });
      const text = (await section.innerText()).replace(/\s+/g, " ");
      expect(text, "the source is named").toContain(t("evidence.sourceYoutube"));
      expect(text, "a running time is shown").toMatch(/\d+:\d{2}/);
    });

    test(`${locale} — a product page carries its own product's video`, async ({ page }) => {
      // Caffe is the clearest case: a restaurant workflow on the restaurant
      // module's page, not in a gallery somewhere else.
      await page.goto(href({ key: "module", module: "bar-restorant" }, locale));
      await expect(page.locator('[data-event="video_play"]')).toHaveCount(1);

      // Budget has no video and no citation, because both payroll
      // walkthroughs are withheld pending a personal-data check. The band is
      // absent rather than empty.
      await page.goto(href({ key: "module", module: "paga" }, locale));
      await expect(page.locator('[data-event="video_play"]')).toHaveCount(0);
      await expect(page.locator("[data-source]")).toHaveCount(0);
    });
  }

  test("the cleared poster is served from this origin, not from Google", async ({ page }) => {
    const requests: string[] = [];
    page.on("request", (r) => requests.push(r.url()));

    await page.goto(href({ key: "home" }, "sq"));
    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
    await page.waitForTimeout(1200);

    expect(
      requests.filter((u) => /ytimg\.com|youtube\.com|google/.test(u)),
      "nothing is requested from Google before a reader presses play",
    ).toEqual([]);

    /*
     * And the poster is actually served. It lives in public/screens/, which
     * the proxy matcher has to exempt by name — an un-exempted folder is not
     * a 404, it is a silent 301 to the home page, and the only symptom is a
     * broken image. That is a real bug this caught.
     */
    const poster = page.locator("img[src*='/screens/']").first();
    await expect(poster).toHaveCount(1);
    const src = (await poster.getAttribute("src")) ?? "";
    const response = await page.request.get(src);
    expect(response.status(), `${src} is served, not redirected`).toBe(200);
    expect(response.headers()["content-type"]).toContain("image");
  });

  test("Facebook is a channel in the navigation, not a card on the home page", async ({ page }) => {
    await page.goto(href({ key: "home" }, "sq"));

    /*
     * In the header and in the footer, and nowhere in the page itself. The
     * header count is "at least one" rather than exactly one because the
     * mobile sheet carries its own copy of the link — on a phone the desktop
     * navigation is still in the DOM, just not displayed.
     */
    const inHeader = await page.locator('header a[href*="facebook.com"]').count();
    expect(inHeader, "Facebook is reachable from the navigation").toBeGreaterThanOrEqual(1);
    await expect(page.locator('footer a[href*="facebook.com"]')).toHaveCount(1);
    await expect(page.locator('main [data-source="facebook"]')).toHaveCount(0);

    // And it is actually reachable at this width, not merely present.
    await expect(
      page.locator('a[href*="facebook.com"]').locator("visible=true").first(),
    ).toBeVisible();
  });
});
