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

import { getModule, getTrail, getTutorials, getUi, tutorialsInYear } from "../lib/content";
import { LOCALES } from "../lib/locales";
import { href, MODULE_IDS, MODULES } from "../lib/routes";

import { PAGES } from "./routes";

/**
 * The brand hierarchy.
 *
 * SULSYS is the company. NEXYS SYSTEM is the software it builds. The first
 * build of this site inverted that — the global lockup read "NEXYS SYSTEM /
 * by SULSYS" — and a visitor could reasonably have concluded the company was
 * called Nexys.
 *
 * These assertions exist so that cannot come back silently. They check the
 * relationship rather than any particular wording, so the copy can still be
 * rewritten freely: what is locked is which name owns the site.
 */

test.describe("the site belongs to Sulsys", () => {
  for (const page of PAGES) {
    test(`${page.name} — the header mark is the company, not the product`, async ({
      page: browserPage,
    }) => {
      await browserPage.goto(page.path);

      // The first link in the header is the home link, and it carries the
      // company's name. Not the product's.
      const mark = browserPage.locator("header a").first();
      const text = (await mark.textContent())?.trim() ?? "";

      expect(text, "the header mark names Sulsys").toMatch(/Sulsys/i);
      expect(text, "the header mark does not name the product").not.toMatch(/Nexys/i);

      // It points at the home page of this locale.
      await expect(mark).toHaveAttribute("href", href({ key: "home" }, page.locale));
    });
  }
});

test.describe("the home page introduces the company before the product", () => {
  for (const locale of LOCALES) {
    const t = (key: string) => getUi(locale)[key];

    test(`${locale} — SULSYS is the first thing on the page`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));

      const company = t("site.companyName");
      const product = t("site.productName");

      // Both names appear in the masthead, and the company comes first in the
      // document — which is also the order a screen reader meets them in.
      const body = (await page.locator("main").innerText()).replace(/\s+/g, " ");
      const companyAt = body.toLowerCase().indexOf(company.toLowerCase());
      const productAt = body.toLowerCase().indexOf(product.toLowerCase());

      expect(companyAt, "the company is named on the home page").toBeGreaterThan(-1);
      expect(productAt, "the product is named on the home page").toBeGreaterThan(-1);
      expect(companyAt, "the company is named before the product").toBeLessThan(productAt);
    });

    test(`${locale} — the masthead carries the company and the product together`, async ({
      page,
    }) => {
      await page.goto(href({ key: "home" }, locale));

      // The opening withholds the index for about a second; what is measured
      // is the hero at rest. The stage is hidden in the server HTML too, so
      // "hidden" alone proves nothing — wait for the opening to arm, then for
      // the stage to leave.
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");
      await expect(page.locator("[data-opening-stage]")).toBeHidden({ timeout: 9000 });

      // The product index links to the product overview from the first
      // screen, so the hierarchy is visible without scrolling. Scoped to
      // <main>, since the header navigation links to the same page from
      // outside it.
      const shelf = page
        .locator(`main a[href="${href({ key: "nexys" }, locale)}"]`)
        .locator("visible=true")
        .first();
      await expect(shelf).toBeVisible();

      const box = await shelf.boundingBox();
      const viewport = page.viewportSize();
      expect(box, "the product shelf has a box").not.toBeNull();
      expect(
        (box?.y ?? 0) + (box?.height ?? 0),
        "company and product are both in the first screen",
      ).toBeLessThanOrEqual((viewport?.height ?? 0) + 1);
    });

    test(`${locale} — the H1 is the company's sentence, not a product slogan`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));
      const h1 = await page.locator("h1").innerText();
      expect(h1, "the home H1 does not lead with the product name").not.toMatch(/^\s*Nexys/i);
    });
  }
});

test.describe("the product knows whose it is", () => {
  for (const locale of LOCALES) {
    test(`${locale} — /nexys says it is a product of Sulsys`, async ({ page }) => {
      await page.goto(href({ key: "nexys" }, locale));
      const t = (key: string) => getUi(locale)[key];
      await expect(page.getByText(t("site.productOf"), { exact: false }).first()).toBeVisible();
    });
  }
});

test.describe("the product family is named in the first screen", () => {
  for (const locale of LOCALES) {
    test(`${locale} — the masthead names all four modules`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));

      // Nexys is a system with parts, and a reader learns that without
      // scrolling. Each name is also the link to that module's page.
      for (const entry of MODULES) {
        const link = page
          .locator(`main a[href="${href({ key: "module", module: entry.id }, locale)}"]`)
          .first();
        await expect(link, `${entry.brandName} is linked from the masthead`).toContainText(
          entry.brandName,
        );
      }
    });
  }
});

test.describe("four module pages, four different products", () => {
  for (const locale of LOCALES) {
    test(`${locale} — every module states its own unit of work`, async ({ page }) => {
      const seen = new Set<string>();

      for (const id of MODULE_IDS) {
        const content = getModule(locale, id);
        const signature = content.unit.join(" · ");

        expect(seen.has(signature), `${id} does not reuse another module's unit`).toBe(false);
        seen.add(signature);

        await page.goto(href({ key: "module", module: id }, locale));
        // Scoped to <main>: the header's module panel carries the same words
        // in a closed disclosure, and matching that proves nothing.
        for (const noun of content.unit) {
          await expect(
            page.locator("main").getByText(noun, { exact: false }).first(),
            `${id} shows "${noun}"`,
          ).toBeVisible();
        }
      }
    });
  }
});

test.describe("the opening is complete before it moves", () => {
  for (const locale of LOCALES) {
    test(`${locale} — with reduced motion nothing is armed and the hero is whole`, async ({
      browser,
    }) => {
      // The opening is an enhancement. A reader who has asked for stillness
      // gets the finished hero: the mark in its corner, the headline, the
      // product index, and no stage.
      const context = await browser.newContext({ reducedMotion: "reduce" });
      const page = await context.newPage();
      await page.goto(href({ key: "home" }, locale));

      await expect(page.locator("[data-armed]")).toHaveCount(0);
      await expect(page.locator("[data-opening-stage]")).toBeHidden();
      await expect(page.locator("header [data-corner-mark]")).toHaveCSS("opacity", "1");

      const opacity = await page
        .locator("h1")
        .evaluate((el) => Number(getComputedStyle(el).opacity));
      expect(opacity, "the headline is at full opacity").toBe(1);

      await context.close();
    });

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

      await expect(page.locator("[data-armed]")).toHaveCount(0);
      await expect(page.locator("[data-opening-stage]")).toBeHidden();
      await expect(page.locator("h1")).toBeVisible();
      await expect(page.locator('header [data-mark="logo"]:not(dialog *)')).toBeVisible();

      await context.close();
    });

    test(`${locale} — the opening plays, lands the mark, and leaves the server's DOM`, async ({
      page,
    }) => {
      await page.goto(href({ key: "home" }, locale));

      // It armed, so it is playing — and within about four seconds it is over
      // and the page is exactly what the server sent, mark in its corner.
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");
      await expect(page.locator("[data-opening-stage]")).toBeVisible();
      await expect(page.locator("[data-opening-stage]")).toBeHidden({ timeout: 9000 });
      await expect(page.locator("header [data-corner-mark]")).toHaveCSS("opacity", "1");
      await expect(page.locator("h1")).toBeVisible();
    });

    test(`${locale} — every fresh arrival plays it: a refresh, and coming back through the site`, async ({
      page,
    }) => {
      await page.goto(href({ key: "home" }, locale));
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");

      // A refresh is a fresh arrival.
      await page.reload();
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");
      await expect(page.locator("[data-opening-stage]")).toBeVisible();

      // So is coming back to the home page through the site's own navigation:
      // the footer's product link out, the corner mark back.
      await page.mouse.wheel(0, 200);
      await expect(page.locator("[data-opening-stage]")).toBeHidden({ timeout: 2000 });
      await page
        .locator(`footer a[href="${href({ key: "nexys" }, locale)}"]`)
        .first()
        .click();
      await page.waitForURL(`**${href({ key: "nexys" }, locale)}`);
      await page.locator("header [data-corner-mark] a").click();
      await page.waitForURL(`**${href({ key: "home" }, locale)}`);
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");
    });

    test(`${locale} — a scroll resolves it to the finished page at once`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));
      // The stage is on screen from the first paint; the interrupt listens
      // from the moment the opening arms, which is what a reader's scroll
      // actually meets.
      await expect(page.locator("[data-opening]")).toHaveAttribute("data-armed", "true");
      await expect(page.locator("[data-opening-stage]")).toBeVisible();
      await page.mouse.wheel(0, 120);
      await expect(page.locator("[data-opening-stage]")).toBeHidden({ timeout: 1000 });
      await expect(page.locator("header [data-corner-mark]")).toHaveCSS("opacity", "1");
    });
  }
});

test.describe("the trail counts what is actually in the library", () => {
  for (const locale of LOCALES) {
    test(`${locale} — every video count matches tutorials/index.json`, async ({ page }) => {
      await page.goto(href({ key: "company" }, locale));
      const trail = getTrail(locale);
      const tutorials = getTutorials();
      const t = (key: string) => getUi(locale)[key];

      for (const entry of trail.entries) {
        await expect(page.getByText(entry.year, { exact: false }).first()).toBeVisible();
        if (!entry.countsTutorials) continue;

        const n = tutorialsInYear(tutorials, entry.year);
        expect(n, `${entry.year} has listed videos`).toBeGreaterThan(0);
        await expect(
          page.getByText(t("trail.videos").replace("{n}", String(n)), { exact: false }).first(),
        ).toBeVisible();
      }
    });
  }
});

test.describe("the company closes every page title", () => {
  for (const page of PAGES) {
    test(`${page.name} — the title ends with Sulsys`, async ({ page: browserPage }) => {
      await browserPage.goto(page.path);
      const title = await browserPage.title();
      expect(title, "the tab says whose site this is").toMatch(/Sulsys\s*$/);
    });
  }
});

/**
 * The mark itself.
 *
 * V2 carried a typographic stand-in because no vector of the real identity
 * existed. It does now, and these assertions keep it in place: the company's
 * actual artwork on every page, the heritage tagline confined to the two
 * places whose subject is the company, and nothing invented standing in for
 * the product.
 */
test.describe("the real Sulsys mark is on the site", () => {
  for (const page of PAGES) {
    test(`${page.name} — the header carries the mark, not a stand-in`, async ({
      page: browserPage,
    }) => {
      await browserPage.goto(page.path);

      /*
       * The corner mark, which is `:not(dialog *)` because the mobile sheet
       * restates the mark inside its own dialog — deliberately, and it is in
       * the DOM at every width whether the sheet is open or not. Asserting a
       * bare count of two would pass for the wrong reasons; this says the one
       * thing meant here, that the header's own corner carries exactly one
       * mark, and the next assertion covers the sheet's.
       */
      const mark = browserPage.locator('header [data-mark="logo"]:not(dialog *)');
      await expect(mark, "the header corner shows the Sulsys logo").toHaveCount(1);
      await expect(
        browserPage.locator('header dialog [data-mark="logo"]'),
        "the mobile sheet restates the mark",
      ).toHaveCount(1);

      // Both halves of the real artwork, each drawn from a real path.
      for (const part of ["symbol", "wordmark"]) {
        const d = await browserPage
          .locator(`header [data-mark="${part}"]:not(dialog *) path`)
          .first()
          .getAttribute("d");
        expect(d?.length ?? 0, `the header ${part} is drawn geometry`).toBeGreaterThan(200);
      }

      // And it still announces the company, because the artwork is decorative
      // and the name beside it is the accessible one.
      await expect(browserPage.locator("header a").first()).toHaveAttribute(
        "href",
        href({ key: "home" }, page.locale),
      );
    });
  }
});

test.describe("the heritage tagline is used sparingly", () => {
  for (const locale of LOCALES) {
    test(`${locale} — the tagline appears on the company page and the footer only`, async ({
      page,
    }) => {
      // V4.1: the opening introduced the mark and the header carries it; the
      // footer signs in type. The complete heritage identity — blades,
      // wordmark, tagline — appears on the company page, and nowhere else.
      await page.goto(href({ key: "home" }, locale));
      await expect(page.locator('[data-mark="lockup"]')).toHaveCount(0);
      await expect(
        page.locator('footer [data-mark="logo"], footer [data-mark="stack"]'),
      ).toHaveCount(0);

      // The company page is the one page that also states it in the open.
      await page.goto(href({ key: "company" }, locale));
      await expect(page.locator('main [data-mark="lockup"]')).toHaveCount(1);
    });

    test(`${locale} — the masthead signs the page without the tagline`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));
      await expect(page.locator('main [data-mark="logo"]')).toHaveCount(1);
      await expect(page.locator('main [data-mark="tagline"]')).toHaveCount(0);
    });
  }
});

test.describe("nothing invented stands in for the product", () => {
  for (const locale of LOCALES) {
    test(`${locale} — the home page carries no fabricated Nexys mark`, async ({ page }) => {
      await page.goto(href({ key: "home" }, locale));

      /*
       * The only public "Nexys mark" is 2016 box art the asset manifest marks
       * DO NOT USE, so the product is set in type until Sulsys supplies a real
       * vector. Every piece of drawn artwork in <main> must therefore be part
       * of the company's own mark — if a new decorative glyph appears beside
       * the product, this is the test that notices.
       */
      const marks = await page
        .locator("main svg[data-mark]")
        .evaluateAll((els) => els.map((el) => el.getAttribute("data-mark")));
      for (const mark of marks) {
        expect(["symbol", "wordmark", "tagline", "lockup", "stack"]).toContain(mark);
      }

      // The product is still named, linked and first-screen — just typeset.
      const shelf = page.locator(`main a[href="${href({ key: "nexys" }, locale)}"]`).first();
      await expect(shelf).toContainText(getUi(locale)["site.productName"]);
    });
  }
});
