import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { runSourceSyncCheck, parseModelsSection } from "./sync-check-source";

describe("runSourceSyncCheck", () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sync-check-"));
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it("returns 0 for an empty directory with no sources or docs", async () => {
    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("reports missing doc when source exists without doc", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "command");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(1);
    expect(result.errors).toContainEqual(
      expect.objectContaining({ type: "missing-doc", expectedBasename: "createorder" }),
    );
  });

  it("reports orphaned doc when doc exists without source", async () => {
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "commands");
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(1);
    expect(result.errors).toContainEqual(
      expect.objectContaining({ type: "orphaned-doc", expectedBasename: "createorder" }),
    );
  });

  it("returns 0 when source and doc match", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "command");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "commands");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("excludes test files from source matching", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "command");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "createOrder.test.ts"), "test()");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("uses custom modulesRoot to rewrite glob patterns", async () => {
    const srcDir = path.join(tmpDir, "custom-mods", "foo", "command");
    const docDir = path.join(tmpDir, "custom-mods", "foo", "docs", "commands");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");

    const result = await runSourceSyncCheck({ modulesRoot: "custom-mods" }, tmpDir);
    expect(result.exitCode).toBe(0);
    expect(result.summary.totalSources).toBe(1);
  });

  it("uses custom appRoot to rewrite glob patterns", async () => {
    const srcDir = path.join(tmpDir, "apps", "app1", "backend", "src", "resolvers");
    const docDir = path.join(tmpDir, "apps", "app1", "docs", "resolver");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "myResolver.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "myResolver.md"), "# MyResolver");

    const result = await runSourceSyncCheck({ appRoot: "apps/app1" }, tmpDir);
    expect(result.exitCode).toBe(0);
    expect(result.summary.totalSources).toBe(1);
  });

  it("reports missing doc for query source without doc", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "query");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(1);
    expect(result.errors).toContainEqual(
      expect.objectContaining({
        type: "missing-doc",
        category: "query",
        expectedBasename: "convertamount",
      }),
    );
  });

  it("returns 0 when query source and doc match", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "query");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "queries");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "convertAmount.md"), "# ConvertAmount");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
  });

  it("matches generated query files to docs by stripping .generated suffix", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "query");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "queries");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "getCurrency.generated.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "GetCurrency.md"), "# GetCurrency");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("excludes generated command files from source matching", async () => {
    const srcDir = path.join(tmpDir, "modules", "foo", "command");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "commands");
    fs.mkdirSync(srcDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
    fs.writeFileSync(path.join(srcDir, "createOrder.generated.ts"), "export {}");
    fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
  });

  it("checks only module patterns when appRoot is not set", async () => {
    const resolverSrcDir = path.join(
      tmpDir,
      "examples",
      "app1",
      "backend",
      "src",
      "modules",
      "m1",
      "resolvers",
    );
    fs.mkdirSync(resolverSrcDir, { recursive: true });
    fs.writeFileSync(path.join(resolverSrcDir, "myResolver.ts"), "export {}");

    const result = await runSourceSyncCheck({ modulesRoot: "modules", appRoot: undefined }, tmpDir);
    // Without appRoot, app patterns are not checked
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("matches db files against ### Models section entries (many-to-one)", async () => {
    const dbDir = path.join(tmpDir, "modules", "foo", "db");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "models");
    fs.mkdirSync(dbDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(dbDir, "order.ts"), "export {}");
    fs.writeFileSync(path.join(dbDir, "orderLine.ts"), "export {}");
    fs.writeFileSync(
      path.join(docDir, "Order.md"),
      "# Order\n\n### Models\n\n- Order\n- OrderLine\n\n### Invariants\n",
    );

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(0);
    expect(result.errors).toHaveLength(0);
  });

  it("reports missing-doc when db file has no matching ### Models entry", async () => {
    const dbDir = path.join(tmpDir, "modules", "foo", "db");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "models");
    fs.mkdirSync(dbDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(dbDir, "order.ts"), "export {}");
    fs.writeFileSync(path.join(dbDir, "orderLine.ts"), "export {}");
    fs.writeFileSync(
      path.join(docDir, "Order.md"),
      "# Order\n\n### Models\n\n- Order\n\n### Invariants\n",
    );

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(1);
    expect(result.errors).toContainEqual(
      expect.objectContaining({
        type: "missing-doc",
        category: "model",
        expectedBasename: "orderline",
      }),
    );
  });

  it("reports orphaned-doc when ### Models entry has no db file", async () => {
    const dbDir = path.join(tmpDir, "modules", "foo", "db");
    const docDir = path.join(tmpDir, "modules", "foo", "docs", "models");
    fs.mkdirSync(dbDir, { recursive: true });
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(dbDir, "order.ts"), "export {}");
    fs.writeFileSync(
      path.join(docDir, "Order.md"),
      "# Order\n\n### Models\n\n- Order\n- OrderLine\n\n### Invariants\n",
    );

    const result = await runSourceSyncCheck(
      { modulesRoot: "modules", appRoot: "examples" },
      tmpDir,
    );
    expect(result.exitCode).toBe(1);
    expect(result.errors).toContainEqual(
      expect.objectContaining({
        type: "orphaned-doc",
        category: "model",
        expectedBasename: "orderline",
      }),
    );
  });
});

describe("parseModelsSection", () => {
  it("extracts model names from ### Models section", () => {
    const content =
      "# Order\n\n### Models\n\n- Order\n- OrderLine\n- OrderSnapshot\n\n### Invariants\n";
    expect(parseModelsSection(content)).toEqual(["Order", "OrderLine", "OrderSnapshot"]);
  });

  it("returns empty array when no ### Models section exists", () => {
    expect(parseModelsSection("# Order\n\n### Invariants\n")).toEqual([]);
  });

  it("stops at next heading", () => {
    const content = "### Models\n\n- Foo\n\n### Invariants\n\n- Bar\n";
    expect(parseModelsSection(content)).toEqual(["Foo"]);
  });
});
