import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { appComposeCategories } from "../../app/sync-check/categories";
import { moduleCategories } from "../../module/sync-check/categories";
import type { CategoryConfig } from "./shared";
import { runSourceSyncCheck } from "./source";

function buildCategories(opts: { modulesRoot?: string; appRoot?: string }): CategoryConfig[] {
  return [
    ...(opts.modulesRoot ? moduleCategories(opts.modulesRoot) : []),
    ...(opts.appRoot ? appComposeCategories(opts.appRoot) : []),
  ];
}

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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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", "command");
    fs.mkdirSync(docDir, { recursive: true });
    fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");

    const result = await runSourceSyncCheck(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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", "command");
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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", "command");
    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(
      buildCategories({ modulesRoot: "custom-mods" }),
      tmpDir,
    );
    expect(result.errors).toHaveLength(0);
    expect(result.filesChecked).toBe(2);
  });

  it("uses custom appRoot to rewrite glob patterns", async () => {
    const srcDir = path.join(tmpDir, "apps", "app1", "backend", "src", "resolver");
    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(buildCategories({ appRoot: "apps/app1" }), tmpDir);
    expect(result.errors).toHaveLength(0);
    expect(result.filesChecked).toBe(2);
  });

  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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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", "query");
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    expect(result.errors).toHaveLength(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", "query");
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    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", "command");
    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(
      buildCategories({ modulesRoot: "modules", appRoot: "examples" }),
      tmpDir,
    );
    expect(result.errors).toHaveLength(0);
  });

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

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