import { describe, expect, it } from "vitest";
import { makeTokenDefinitionFact, type GovernanceConfig } from "@fragments-sdk/core";
import { scanWithFacts } from "../typestyle/check.js";
import { parseSuppressionDirectives } from "../ucf/directives.js";
import { createMemoryScanSource } from "./memory-source.js";
import { scanSourceWithFacts } from "./scan-source.js";
import type { ScanSource } from "./types.js";

const policy: GovernanceConfig = {
  severity: "error",
  rules: { "tokens/hardcoded-values": { enabled: true, severity: "error" } },
  styles: [
    {
      kind: "style.rawColors.forbid",
      except: ["transparent", "currentColor", "inherit"],
      prefer: "token",
      severity: "error",
    },
  ],
};

const tokens = [
  makeTokenDefinitionFact({
    name: "--color-brand-600",
    value: "#2563eb",
    category: "color",
  }),
];

const facts = { governConfig: policy, tokens };

const files = {
  "src/Button.module.scss": ".button { color: #2563eb; }\n",
  "src/ignored.md": "color: #2563eb\n",
};

describe("scanSourceWithFacts", () => {
  it("matches scanWithFacts for the same in-memory files", async () => {
    const sourceResult = await scanSourceWithFacts({
      source: createMemoryScanSource(files),
      facts,
      files: [{ relPath: "src/Button.module.scss" }],
    });
    const directResult = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Button.module.scss",
          relPath: "src/Button.module.scss",
          content: files["src/Button.module.scss"],
        },
      ],
    });

    expect(sourceResult.findings.map((finding) => finding.fingerprint)).toEqual(
      directResult.findings.map((finding) => finding.fingerprint)
    );
    expect(sourceResult.filesScanned).toEqual(directResult.filesScanned);
    expect(sourceResult.sourceByFile.get("src/Button.module.scss")).toBe(
      files["src/Button.module.scss"]
    );
  });

  it("skips unsupported extensions and missing files", async () => {
    const result = await scanSourceWithFacts({
      source: createMemoryScanSource(files),
      facts,
      files: [{ relPath: "src/ignored.md" }, { relPath: "src/missing.scss" }],
    });

    expect(result.findings).toEqual([]);
    expect(result.filesScanned).toEqual([]);
    expect([...result.sourceByFile.keys()]).toEqual([]);
  });

  it("scans TypeScript style modules for CSS-in-JS declarations", async () => {
    const result = await scanSourceWithFacts({
      source: createMemoryScanSource({
        "src/styles.ts": [
          "import { style } from '@vanilla-extract/css';",
          "export const button = style({ color: '#2563eb' });",
        ].join("\n"),
      }),
      facts,
      files: [{ relPath: "src/styles.ts" }],
    });

    expect(result.filesScanned).toEqual(["src/styles.ts"]);
    expect(result.findings).toEqual([
      expect.objectContaining({
        ruleId: "styles/no-raw-color",
        location: expect.objectContaining({ file: "src/styles.ts", line: 2 }),
      }),
    ]);
  });

  it("normalizes backslash paths before reading and reporting", async () => {
    const result = await scanSourceWithFacts({
      source: createMemoryScanSource(files),
      facts,
      files: [{ relPath: "src\\Button.module.scss" }],
    });

    expect(result.filesScanned).toEqual(["src/Button.module.scss"]);
    expect(result.findings[0]?.location.file).toBe("src/Button.module.scss");
  });

  it("reads supported files concurrently while preserving scan order", async () => {
    const sourceFiles = {
      "src/slow.scss": ".slow { color: #2563eb; }\n",
      "src/fast.scss": ".fast { color: #2563eb; }\n",
      "src/third.scss": ".third { color: #2563eb; }\n",
    };
    let activeReads = 0;
    let maxActiveReads = 0;
    const source: ScanSource = {
      kind: "memory",
      root: "/repo",
      async readFile(path) {
        activeReads += 1;
        maxActiveReads = Math.max(maxActiveReads, activeReads);
        await new Promise((resolve) => {
          setTimeout(resolve, path.includes("slow") ? 20 : 0);
        });
        activeReads -= 1;
        return sourceFiles[path as keyof typeof sourceFiles] ?? null;
      },
    };

    const result = await scanSourceWithFacts({
      source,
      facts,
      files: [
        { relPath: "src/slow.scss" },
        { relPath: "src/fast.scss" },
        { relPath: "src/third.scss" },
      ],
    });

    expect(maxActiveReads).toBeGreaterThan(1);
    expect(result.filesScanned).toEqual(["src/slow.scss", "src/fast.scss", "src/third.scss"]);
  });

  it("dedupes normalized source paths before reading", async () => {
    const reads: string[] = [];
    const source: ScanSource = {
      kind: "memory",
      root: "/repo",
      async readFile(path) {
        reads.push(path);
        return files[path as keyof typeof files] ?? null;
      },
    };

    const result = await scanSourceWithFacts({
      source,
      facts,
      files: [
        { relPath: "src/Button.module.scss" },
        { relPath: "./src/Button.module.scss" },
        { relPath: "src\\Button.module.scss" },
      ],
    });

    expect(reads).toEqual(["src/Button.module.scss"]);
    expect(result.filesScanned).toEqual(["src/Button.module.scss"]);
  });

  it("rejects unsafe paths before reading", async () => {
    const result = await scanSourceWithFacts({
      source: createMemoryScanSource(files),
      facts,
      files: [{ relPath: "../Button.module.scss" }, { relPath: "/src/Button.module.scss" }],
    });

    expect(result.findings).toEqual([]);
    expect(result.filesScanned).toEqual([]);
  });

  it("preserves suppression behavior through source scanning", async () => {
    const directResult = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Button.module.scss",
          relPath: "src/Button.module.scss",
          content: files["src/Button.module.scss"],
        },
      ],
    });
    const code = directResult.findings[0]?.code;
    expect(code).toMatch(/^FUI\d{4}$/);

    const suppressed = [
      `// @fragments-expect-error ${code} reason="legacy" expires="2099-01-01"`,
      files["src/Button.module.scss"],
    ].join("\n");
    const result = await scanSourceWithFacts({
      source: createMemoryScanSource({ "src/Button.module.scss": suppressed }),
      facts,
      files: [{ relPath: "src/Button.module.scss" }],
    });

    expect(result.suppressions).toHaveLength(1);
    expect(result.suppressedFindings).toHaveLength(1);
    expect(result.findings.map((finding) => finding.ruleId)).not.toContain("styles/no-raw-color");
  });
});

describe("fragments-allow directives", () => {
  function findingCode(): string {
    const directResult = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Button.module.scss",
          relPath: "src/Button.module.scss",
          content: files["src/Button.module.scss"],
        },
      ],
    });
    const code = directResult.findings[0]?.code;
    expect(code).toMatch(/^FUI\d{4}$/);
    return String(code);
  }

  it("parses fragments-allow with a code, a free-text reason, and the allow flavor", () => {
    const directives = parseSuppressionDirectives({
      filePath: "src/checkout/PromoBanner.tsx",
      source: [
        "// fragments-allow FUI4001: legacy promo banner, replacement tracked in #482",
        "export const x = 1;",
        "",
      ].join("\n"),
    });

    expect(directives).toHaveLength(1);
    expect(directives[0]).toMatchObject({
      code: "FUI4001",
      reason: "legacy promo banner, replacement tracked in #482",
      directive: "allow",
      attachedLine: 2,
    });
    expect(directives[0].expiresOn).toBeUndefined();
  });

  it("keeps expect-error facts free of the discriminator (legacy shape unchanged)", () => {
    const directives = parseSuppressionDirectives({
      filePath: "src/a.scss",
      source: ['// @fragments-expect-error FUI2005 reason="legacy"', ".a { color: red; }", ""].join(
        "\n"
      ),
    });

    expect(directives).toHaveLength(1);
    expect(directives[0].directive).toBeUndefined();
  });

  it("suppresses a matching finding without emitting missing-expiry", () => {
    const code = findingCode();
    const suppressed = [
      `// fragments-allow ${code}: legacy promo banner, replacement tracked in #482`,
      files["src/Button.module.scss"],
    ].join("\n");

    const result = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Button.module.scss",
          relPath: "src/Button.module.scss",
          content: suppressed,
        },
      ],
    });

    expect(result.suppressions).toHaveLength(1);
    expect(result.suppressedFindings).toHaveLength(1);
    expect(result.suppressedFindings[0].directive.reason).toBe(
      "legacy promo banner, replacement tracked in #482"
    );
    expect(result.findings).toEqual([]);
  });

  it("ignores a malformed fragments-allow flag and lets the rule fire", () => {
    const malformed = ["// fragments-allow: oops", files["src/Button.module.scss"]].join("\n");

    const result = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Button.module.scss",
          relPath: "src/Button.module.scss",
          content: malformed,
        },
      ],
    });

    expect(result.suppressions).toHaveLength(0);
    expect(result.suppressedFindings).toHaveLength(0);
    expect(result.findings.map((finding) => finding.ruleId)).toContain("styles/no-raw-color");
  });

  it("still flags an unused fragments-allow directive", () => {
    const result = scanWithFacts({
      facts,
      files: [
        {
          absPath: "/repo/src/Clean.module.scss",
          relPath: "src/Clean.module.scss",
          content: [
            "// fragments-allow FUI2005: nothing to suppress here",
            ".clean { color: transparent; }",
            "",
          ].join("\n"),
        },
      ],
    });

    expect(result.findings.map((finding) => finding.ruleId)).toEqual(["unused-suppression"]);
  });
});
