import { describe, expect, it } from "vitest";

import { classnameFacts as classNameFacts } from "./helpers.js";

describe("extractClassnames — ACCEPTANCE §1.1–§1.6", () => {
  it("§1.1 — plain literal className emits one classname_literal", () => {
    const facts = classNameFacts(
      `export default function P() {
        return <div className="m-4 bg-red-500">child</div>;
      }`,
    );

    expect(facts).toHaveLength(1);
    expect(facts[0]).toMatchObject({
      kind: "classname_literal",
      origin: "attribute",
      classes: ["m-4", "bg-red-500"],
      attr: "className",
    });
  });

  it("§1.2 — clsx with literal + identifier + conditional", () => {
    const facts = classNameFacts(
      `export default function P({ isActive, other }: any) {
        return <div className={clsx('p-4', isActive && 'bg-blue-500', other)}>x</div>;
      }`,
    );

    const literal = facts.find((f) => f.kind === "classname_literal");
    const dynamic = facts.find((f) => f.kind === "classname_dynamic");

    expect(literal).toBeDefined();
    expect(dynamic).toBeDefined();
    expect(literal).toMatchObject({
      kind: "classname_literal",
      origin: "clsx",
      classes: ["p-4", "bg-blue-500"],
    });
    expect(dynamic).toMatchObject({
      kind: "classname_dynamic",
      reason: "identifier",
      snippet: "other",
    });
  });

  it("§1.3 — cn with object form", () => {
    const facts = classNameFacts(
      `export default function P({ active }: any) {
        return <div className={cn('p-4', { 'bg-red-500': active })}>x</div>;
      }`,
    );

    const literal = facts.find((f) => f.kind === "classname_literal");
    expect(literal).toMatchObject({
      kind: "classname_literal",
      origin: "cn",
      classes: ["p-4", "bg-red-500"],
    });
  });

  it("§1.4 — template literal interpolation", () => {
    const facts = classNameFacts(
      "export default function P({ size }: any) {\n" +
        "  return <div className={`p-${size} text-red-500`}>x</div>;\n" +
        "}",
    );

    const literal = facts.find((f) => f.kind === "classname_literal");
    const dynamic = facts.find((f) => f.kind === "classname_dynamic");

    expect(literal).toMatchObject({
      kind: "classname_literal",
      origin: "template",
      classes: ["text-red-500"],
    });
    expect(dynamic).toMatchObject({
      kind: "classname_dynamic",
      reason: "template_interpolation",
    });
  });

  it("§1.5 — cva() opacity (no recursion into variants)", () => {
    const facts = classNameFacts(
      `export default function P() {
        return <div className={buttonVariants({ variant: 'destructive' })}>x</div>;
      }`,
    );

    expect(facts).toHaveLength(1);
    expect(facts[0]).toMatchObject({
      kind: "classname_dynamic",
      reason: "unknown_helper",
    });
  });

  it("§1.5 — cva when invoked directly", () => {
    const facts = classNameFacts(
      `export default function P() {
        return <div className={cva({ variant: 'destructive' })}>x</div>;
      }`,
    );

    expect(facts).toHaveLength(1);
    expect(facts[0]).toMatchObject({
      kind: "classname_dynamic",
      reason: "cva-arg",
    });
    if (facts[0].kind === "classname_dynamic") {
      expect(facts[0].snippet).toContain("cva");
      expect(facts[0].snippet).toContain("destructive");
    }
  });

  it("§1.6 — bare identifier is dynamic", () => {
    const facts = classNameFacts(
      `export default function P({ someVar }: any) {
        return <div className={someVar}>x</div>;
      }`,
    );

    expect(facts).toHaveLength(1);
    expect(facts[0]).toMatchObject({
      kind: "classname_dynamic",
      reason: "identifier",
      snippet: "someVar",
    });
  });
});

describe("extractClassnames — locally unique facts (architecture-spine defense)", () => {
  it("emits stable, distinct fact ids for many sibling className facts on one element", () => {
    const facts = classNameFacts(
      `export default function P({ a, b, c }: any) {
        return <div className={cn('p-4', a, b, { 'bg-red-500': c })}>x</div>;
      }`,
    );

    const ids = facts.map((f) => f.id);
    expect(new Set(ids).size).toBe(ids.length);
  });

  it("does not collide with sibling classname facts across elements", () => {
    const facts = classNameFacts(
      `export default function P() {
        return (
          <div>
            <a className="text-blue-500 underline" />
            <a className="text-blue-500 underline" />
            <a className="text-blue-500 underline" />
          </div>
        );
      }`,
    );

    const literals = facts.filter((f) => f.kind === "classname_literal");
    expect(literals).toHaveLength(3);
    const ids = literals.map((f) => f.id);
    expect(new Set(ids).size).toBe(3);
  });
});

describe("extractClassnames — recognizedClassHelpers knob", () => {
  it("recurses into a custom helper when registered (replace, not merge)", () => {
    const source = `export default function P({ active }: any) {
      return <div className={mergeClasses('p-4', active && 'bg-red-500')}>x</div>;
    }`;

    expect(classNameFacts(source)).toEqual([
      expect.objectContaining({
        kind: "classname_dynamic",
        reason: "unknown_helper",
      }),
    ]);

    const withCustom = classNameFacts(source, undefined, {
      recognizedClassHelpers: ["mergeClasses"],
    });
    const literal = withCustom.find((f) => f.kind === "classname_literal");
    expect(literal).toMatchObject({
      kind: "classname_literal",
      classes: ["p-4", "bg-red-500"],
    });
  });
});

describe("extractClassnames — const-folding (BUG 5)", () => {
  it("folds a hoisted const conditional referenced via template interpolation", () => {
    const facts = classNameFacts(
      "export default function P({ cond }: any) {\n" +
        "  const tone = cond ? 'bg-[#abcdef] text-white' : 'bg-brand';\n" +
        "  return <div className={`p-md ${tone}`}>x</div>;\n" +
        "}",
    );
    const literals = facts.filter((f) => f.kind === "classname_literal");
    const allClasses = literals.flatMap((f) =>
      f.kind === "classname_literal" ? f.classes : [],
    );
    // The arbitrary color hidden in the ternary branch is now surfaced.
    expect(allClasses).toContain("bg-[#abcdef]");
    expect(allClasses).toContain("bg-brand");
  });

  it("folds a hoisted const directly referenced as the className", () => {
    const facts = classNameFacts(
      "export default function P() {\n" +
        "  const classes = 'p-4 bg-[#123456]';\n" +
        "  return <div className={classes}>x</div>;\n" +
        "}",
    );
    const allClasses = facts.flatMap((f) =>
      f.kind === "classname_literal" ? f.classes : [],
    );
    expect(allClasses).toContain("bg-[#123456]");
  });

  it("leaves a non-const (reassigned let) binding dynamic", () => {
    const facts = classNameFacts(
      "export default function P({ cond }: any) {\n" +
        "  let tone = 'bg-brand';\n" +
        "  if (cond) tone = 'bg-[#abcdef]';\n" +
        "  return <div className={tone}>x</div>;\n" +
        "}",
    );
    expect(facts.some((f) => f.kind === "classname_dynamic")).toBe(true);
  });
});

describe("extractClassnames — Ink baseline (§6.34)", () => {
  it("emits zero classname facts for prop-based color (no className usage)", () => {
    const source = `import { Box } from 'ink';

export default function P() {
  return <Box color="red" paddingX={2}>hello</Box>;
}`;

    const facts = classNameFacts(source);
    expect(facts).toHaveLength(0);
  });
});
