import { mkdtemp, writeFile, access } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, it, vi } from "vitest";

import {
  FactIndex,
  makeTailwindClassFact,
  makeThemeDeclarationFact,
  makeUsageNodeFact,
} from "@fragments-sdk/core";

import { resolveTailwindTokenFacts } from "../v4-theme.js";

const FILE = "src/page.tsx";

function addClass(ix: FactIndex, raw: string, utility: string, token: string) {
  const node = makeUsageNodeFact({
    file: FILE,
    nodePath: `0:${raw}`,
    element: "div",
    location: { file: FILE, line: 1, column: 0 },
  });
  ix.add(node);
  ix.add(
    makeTailwindClassFact({
      file: FILE,
      nodeId: node.id,
      raw,
      originPath: `attribute#0/class#${raw}`,
      prefix: null,
      modifiers: [],
      important: false,
      negative: false,
      utility,
      value: { kind: "token", token },
      location: { file: FILE, line: 1, column: 12 },
    })
  );
}

function addTheme(
  ix: FactIndex,
  input: {
    name: string;
    value: string;
    inline?: boolean;
    declarationPath?: string;
  }
) {
  ix.add(
    makeThemeDeclarationFact({
      file: "app/globals.css",
      name: input.name,
      value: input.value,
      declarationPath: input.declarationPath ?? input.name,
      inline: input.inline ?? false,
      flavor: input.inline ? "inline" : "default",
      location: { file: "app/globals.css", line: 2, column: 2 },
    })
  );
}

describe("Tailwind v4 theme resolver", () => {
  it("§4.23 — resolves project-defined tokens from theme-css", () => {
    const ix = new FactIndex();
    addTheme(ix, { name: "--color-brand-500", value: "#abc" });
    addClass(ix, "bg-brand-500", "bg", "brand-500");

    expect(resolveTailwindTokenFacts(ix)).toEqual([
      expect.objectContaining({
        kind: "tailwind_token_resolved",
        utility: "bg",
        token: "brand-500",
        resolved: { kind: "color", value: "#abc", source: "theme-css" },
      }),
    ]);
  });

  it("§4.24 — resolves default palette tokens", () => {
    const ix = new FactIndex();
    addClass(ix, "bg-red-500", "bg", "red-500");

    expect(resolveTailwindTokenFacts(ix)).toEqual([
      expect.objectContaining({
        utility: "bg",
        token: "red-500",
        resolved: { kind: "color", value: "#ef4444", source: "default" },
      }),
    ]);
  });

  it("§4.25 — emits unknown facts for unresolved tokens", () => {
    const ix = new FactIndex();
    addClass(ix, "bg-mystery-123", "bg", "mystery-123");

    expect(resolveTailwindTokenFacts(ix)).toEqual([
      expect.objectContaining({
        utility: "bg",
        token: "mystery-123",
        resolved: { kind: "unknown", source: "unknown" },
      }),
    ]);
  });

  it("§4.26 — preserves inline theme provenance", () => {
    const ix = new FactIndex();
    addTheme(ix, { name: "--color-brand-500", value: "#abc", inline: true });
    addClass(ix, "bg-brand-500", "bg", "brand-500");

    expect(resolveTailwindTokenFacts(ix)[0]).toMatchObject({
      resolved: { kind: "color", value: "#abc", source: "theme-css", inline: true },
    });
  });

  it("resolves v4 --spacing multiplier tokens", () => {
    const ix = new FactIndex();
    addTheme(ix, { name: "--spacing", value: "0.25rem" });
    addClass(ix, "p-3", "p", "3");

    expect(resolveTailwindTokenFacts(ix)[0]).toMatchObject({
      resolved: { kind: "length", value: "0.75rem", source: "theme-css" },
    });
  });

  it("uses the last @theme declaration and warns once on conflicts", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
    try {
      const ix = new FactIndex();
      addTheme(ix, {
        name: "--color-brand-500",
        value: "#111111",
        declarationPath: "theme#0",
      });
      addTheme(ix, {
        name: "--color-brand-500",
        value: "#222222",
        declarationPath: "theme#1",
      });
      addTheme(ix, {
        name: "--color-brand-500",
        value: "#333333",
        declarationPath: "theme#2",
      });
      addClass(ix, "bg-brand-500", "bg", "brand-500");

      expect(resolveTailwindTokenFacts(ix)[0]).toMatchObject({
        resolved: { kind: "color", value: "#333333", source: "theme-css" },
      });
      expect(warn).toHaveBeenCalledTimes(1);
      expect(warn.mock.calls[0]?.[0]).toContain("--color-brand-500");
    } finally {
      warn.mockRestore();
    }
  });

  it("dedupes repeated utility/token pairs", () => {
    const ix = new FactIndex();
    addClass(ix, "bg-red-500", "bg", "red-500");
    addClass(ix, "hover:bg-red-500", "bg", "red-500");

    expect(resolveTailwindTokenFacts(ix)).toHaveLength(1);
  });

  it("§4.27 — never executes or imports tailwind.config.ts", async () => {
    const root = await mkdtemp(join(tmpdir(), "tailwind-config-trap-"));
    const marker = join(root, "executed");
    await writeFile(
      join(root, "tailwind.config.ts"),
      `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(marker)}, "boom");\n`,
      "utf-8"
    );

    const ix = new FactIndex();
    addClass(ix, "bg-red-500", "bg", "red-500");
    expect(resolveTailwindTokenFacts(ix)).toHaveLength(1);
    await expect(access(marker)).rejects.toThrow();
  });
});
