import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { collectMeasureStructure } from "./structure";

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

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

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

  function writeFile(relativePath: string, content: string) {
    const fullPath = path.join(tmpDir, relativePath);
    fs.mkdirSync(path.dirname(fullPath), { recursive: true });
    fs.writeFileSync(fullPath, content);
  }

  function mkdir(relativePath: string) {
    fs.mkdirSync(path.join(tmpDir, relativePath), { recursive: true });
  }

  it("returns empty result when no roots provided", async () => {
    const result = await collectMeasureStructure({}, tmpDir);
    expect(result).toEqual({});
  });

  it("reports missing required module dirs", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    // missing lib/ (required); query/ is optional and absent
    writeFile("modules/sales/module.ts", "");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    const missing = result.modules?.violations.filter((v) => v.type === "missing-required-dir");
    expect(missing?.length).toBe(1);
    expect(missing?.[0].path.endsWith("lib")).toBe(true);
  });

  it("does not require query/ — command-only modules are valid", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/lib");
    // no query/ directory
    writeFile("modules/sales/module.ts", "");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    expect(result.modules?.violations).toEqual([]);
  });

  it("reports missing app sub-dirs", async () => {
    mkdir("my-app/backend");
    // frontend missing

    const result = await collectMeasureStructure({ appRoot: "my-app" }, tmpDir);
    const missing = result.apps?.violations.filter((v) => v.type === "missing-required-dir");
    expect(missing?.length).toBe(1);
    expect(missing?.[0].path).toContain("frontend");
  });

  it("passes a fully-structured repo", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");

    mkdir("my-app/backend");
    mkdir("my-app/frontend");

    const result = await collectMeasureStructure(
      { appRoot: "my-app", modulesRoot: "modules" },
      tmpDir,
    );
    expect(result.modules?.violations).toEqual([]);
    expect(result.apps?.violations).toEqual([]);
  });

  it("counts modules correctly", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");
    mkdir("modules/promotion/db");
    mkdir("modules/promotion/command");
    mkdir("modules/promotion/query");
    mkdir("modules/promotion/lib");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    expect(result.modules?.count).toBe(2);
  });

  it("flags naming violations at the module root", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");
    writeFile("modules/sales/module.ts", "");
    writeFile("modules/sales/helpers.ts", "");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    const naming = result.modules?.violations.filter((v) => v.type === "naming-violation");
    expect(naming?.some((v) => v.path === "modules/sales/helpers.ts")).toBe(true);
    expect(naming?.some((v) => v.path === "modules/sales/module.ts")).toBe(false);
  });

  it("flags naming violations inside command/, query/, lib/", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");
    mkdir("modules/sales/command");
    writeFile("modules/sales/module.ts", "");
    writeFile("modules/sales/command/CreateOrder.ts", "");
    writeFile("modules/sales/lib/helpers.ts", "");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    const naming = result.modules?.violations.filter((v) => v.type === "naming-violation");
    expect(naming?.some((v) => v.path === "modules/sales/command/CreateOrder.ts")).toBe(true);
    expect(naming?.some((v) => v.path === "modules/sales/lib/helpers.ts")).toBe(true);
  });

  it("routes naming violations under apps/ into the apps bucket", async () => {
    mkdir("apps/x/backend");
    mkdir("apps/x/frontend");
    mkdir("apps/x/legacy");

    const result = await collectMeasureStructure({ appRoot: "apps/x" }, tmpDir);
    const naming = result.apps?.violations.filter((v) => v.type === "naming-violation");
    expect(naming?.some((v) => v.path === "apps/x/legacy")).toBe(true);
  });

  it("does not flag auto-generated .d.ts files", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");
    writeFile("modules/sales/module.ts", "");
    writeFile("modules/sales/tailor.d.ts", "// auto-generated\n");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    const naming = result.modules?.violations.filter((v) => v.type === "naming-violation");
    expect(naming?.some((v) => v.path === "modules/sales/tailor.d.ts")).toBe(false);
  });

  it("accepts canonical naming silently", async () => {
    mkdir("modules/sales/db");
    mkdir("modules/sales/command");
    mkdir("modules/sales/query");
    mkdir("modules/sales/lib");
    writeFile("modules/sales/module.ts", "");
    writeFile("modules/sales/index.ts", "");
    writeFile("modules/sales/db/orders.ts", "");
    writeFile("modules/sales/command/createOrder.ts", "");
    writeFile("modules/sales/lib/types.ts", "");

    const result = await collectMeasureStructure({ modulesRoot: "modules" }, tmpDir);
    const naming = result.modules?.violations.filter((v) => v.type === "naming-violation");
    expect(naming).toEqual([]);
  });
});
