import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import tokens from "../design/design-tokens.json";
import { buildCss, OUTPUT } from "../scripts/build-tokens.mjs";
import { flatten, isColorPrimitive, refsIn, refKey, resolveHex } from "../scripts/lib/tokens.mjs";

const map = flatten(tokens);
const all = [...map.values()];
const primitives = all.filter(isColorPrimitive);
const semanticColours = all.filter((t) => t.path[0] === "color" && !isColorPrimitive(t));
const generated = readFileSync(OUTPUT, "utf8");

describe("design tokens", () => {
  it("has no broken {reference} anywhere", () => {
    const broken = all.flatMap((t) => refsIn(t.value).filter((r: string) => !map.has(refKey(r))).map((r: string) => `${t.key} -> ${r}`));
    expect(broken).toEqual([]);
  });

  it("resolves every semantic colour token to a primitive hex", () => {
    const primitiveHexes = new Set(primitives.map((p) => p.value.toUpperCase()));
    expect(semanticColours.length).toBeGreaterThan(50);
    for (const t of semanticColours) {
      const hex = resolveHex(map, t.key);
      expect(hex, t.key).toMatch(/^#[0-9A-F]{6}$/i);
      expect(primitiveHexes.has(hex.toUpperCase()), `${t.key} -> ${hex} is not a primitive`).toBe(true);
    }
  });

  it("resolves chart and focus colours to primitive hex values too", () => {
    for (const key of [...all.filter((t) => t.path[0] === "chart").map((t) => t.key), "focus.ring-color"]) {
      expect(resolveHex(map, key), key).toMatch(/^#[0-9A-F]{6}$/i);
    }
  });
});

describe("tokens.generated.css", () => {
  it("is up to date with design/design-tokens.json (run npm run tokens:build)", () => {
    expect(generated).toBe(buildCss(tokens));
  });

  it("references only primitives and semantic colours that are defined", () => {
    const defined = new Set([...generated.matchAll(/^\s*(--[\w-]+)\s*:/gm)].map((m) => m[1]));
    const used = [...generated.matchAll(/var\((--[\w-]+)\)/g)].map((m) => m[1]).filter((v) => !v.startsWith("--font-"));
    expect(used.filter((v) => !defined.has(v))).toEqual([]);
  });

  it("keeps primitives out of Tailwind's colour namespace", () => {
    const themeBlock = generated.slice(generated.indexOf("@theme static"));
    expect(themeBlock).not.toMatch(/--color-(white|black|blue|steel|lime|gray|green|amber|red)-/);
  });

  it("makes every numeric text style tabular", () => {
    for (const name of ["numeric-md", "numeric-sm", "numeric-strong", "kpi", "till-line", "till-tender", "till-total"]) {
      expect(generated).toMatch(new RegExp(`@utility text-${name} \{[^}]*tabular-nums`));
    }
  });
});

describe("transition: names that exist in both the legacy globals.css and the Medora tokens", () => {
  // Legacy values win in the app until the legacy @theme blocks are deleted (design/MIGRATION.md).
  // If this list changes, update MIGRATION.md and make sure the change is intended.
  const KNOWN = [
    "--color-bg-selected", "--color-bg-surface", "--color-border-strong", "--color-border-subtle",
    "--color-brand-accent", "--color-brand-primary", "--font-display", "--font-sans",
    "--radius-lg", "--radius-md", "--radius-sm",
  ];
  it("matches the documented collision list", () => {
    const names = (css: string) => new Set([...css.matchAll(/^\s*(--[\w-]+)\s*:/gm)].map((m) => m[1]));
    const legacy = names(readFileSync("app/globals.css", "utf8"));
    const collisions = [...names(generated)].filter((n) => legacy.has(n)).sort();
    expect(collisions).toEqual(KNOWN);
  });
});
