import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
  collectModuleExportNames,
  collectRepositoryCollisions,
  compareExportNameCollisionDebt,
  findAliasingReExports,
  findExportNameCollisions,
  isExcludedExportCollisionSource,
} from "../../scripts/check-export-name-collisions.mts";
import { withTempDir } from "../../src/test-utils/temp-dir.js";

describe("export name collision guard", () => {
  it.each([
    ["src/example.test.ts", true],
    ["src/example.e2e.test.ts", true],
    ["src/example.test-support.ts", true],
    ["src/example.test-helpers.ts", true],
    ["src/example.test-utils.ts", true],
    ["src/example.test-harness.ts", true],
    ["src/example.e2e-harness.ts", true],
    ["src/example.d.ts", true],
    ["src/test/example.ts", true],
    ["src/nested/__fixtures__/example.mts", true],
    ["src/example.ts", false],
    ["src/example.mts", false],
  ])("classifies source exclusion %s", (filePath, expected) => {
    expect(isExcludedExportCollisionSource(filePath)).toBe(expected);
  });

  it("finds exported function and const definitions across modules", () => {
    expect(
      findExportNameCollisions([
        { path: "src/alpha.ts", content: "export function sharedBehavior() {}" },
        { path: "src/beta.ts", content: "export const sharedBehavior = () => {};" },
        {
          path: "src/gamma.ts",
          content: "async function listedBehavior() {}\nexport { listedBehavior };",
        },
        {
          path: "src/delta.mts",
          content: "export async function listedBehavior() {}",
        },
      ]),
    ).toEqual([
      { name: "listedBehavior", files: ["src/delta.mts", "src/gamma.ts"] },
      { name: "sharedBehavior", files: ["src/alpha.ts", "src/beta.ts"] },
    ]);
  });

  it("ignores types, pure re-exports, imports exported locally, and renamed exports", () => {
    const result = collectModuleExportNames(`
      import { importedValue } from "./other.js";
      interface LocalShape {}
      type LocalType = string;
      export { importedValue };
      export { remoteValue } from "./remote.js";
      export { remoteValue as renamedValue } from "./remote.js";
      export * from "./barrel.js";
      export interface ExportedShape {}
      export type ExportedType = string;
    `);
    expect([...result.definitions]).toEqual([]);
    expect([...result.exportedNames]).toEqual(["importedValue", "remoteValue"]);
  });

  it("reports direct aliasing re-exports only outside the Plugin SDK", () => {
    expect(
      findAliasingReExports([
        {
          path: "src/alias.ts",
          content: `
            export { original } from "./source.js";
            export type { OriginalType as RenamedType } from "./source.js";
            export { original as renamed } from "./source.js";
          `,
        },
        {
          path: "src/local-alias.ts",
          content: `
            import { original } from "./source.js";
            export { original as locallyRenamed };
          `,
        },
        {
          path: "src/plugin-sdk/alias.ts",
          content: 'export { original as sanctioned } from "../source.js";',
        },
        {
          path: "packages/support.ts",
          content: 'export { original as packageAlias } from "./source.js";',
          includeDefinitions: false,
        },
      ]),
    ).toEqual([
      {
        exportedName: "renamed",
        importedName: "original",
        line: 4,
        moduleSpecifier: "./source.js",
        path: "src/alias.ts",
      },
    ]);
  });

  it("exempts exact function and const same-name forwarders", () => {
    const forwarders = [
      `
        import { resolveThing as resolveThingImpl } from "./thing.js";
        export function resolveThing(first: string, second?: number) {
          return resolveThingImpl(first, second);
        }
      `,
      `
        import { resolveThing as resolveThingImpl } from "./thing.js";
        export const resolveThing = resolveThingImpl;
      `,
      `
        import { resolveThing as resolveThingImpl } from "./thing.js";
        export const resolveThing = (first: string, second?: number) =>
          resolveThingImpl(first, second);
      `,
      `
        export const runThing = async (...args: unknown[]) => {
          const runtime = await loadRuntime();
          return runtime.runThing(...args);
        };
      `,
      `
        export async function runThing(...args: unknown[]) {
          return (await loadRuntime()).runThing(...args);
        }
      `,
      `
        export async function runThing(...args: unknown[]) {
          const runtime = await loadRuntime();
          return runtime.runThing(...args);
        }
      `,
    ];
    for (const content of forwarders) {
      expect([...collectModuleExportNames(content).definitions]).toEqual([]);
    }
  });

  it.each([
    {
      name: "extra call",
      body: `
        prepare();
        return resolveThingImpl(...args);
      `,
    },
    {
      name: "added argument",
      body: "return resolveThingImpl(...args, fallback);",
    },
    {
      name: "changed argument order",
      params: "first: string, second: string",
      body: "return resolveThingImpl(second, first);",
    },
    {
      name: "layered argument",
      params: "params: Record<string, unknown>",
      body: "return resolveThingImpl({ ...params, enabled: true });",
    },
    {
      name: "conditional",
      body: "return ready ? resolveThingImpl(...args) : fallback;",
    },
  ])("keeps $name wrappers as real definitions", ({ params = "...args: unknown[]", body }) => {
    const result = collectModuleExportNames(`
      import { resolveThing as resolveThingImpl } from "./thing.js";
      export function resolveThing(${params}) {
        ${body}
      }
    `);
    expect([...result.definitions]).toEqual(["resolveThing"]);
  });

  it("keeps const arrows that add arguments as real definitions", () => {
    const result = collectModuleExportNames(`
      import { resolveThing as resolveThingImpl } from "./thing.js";
      export const resolveThing = (...args: unknown[]) => resolveThingImpl(...args, fallback);
    `);
    expect([...result.definitions]).toEqual(["resolveThing"]);
  });

  it("discovers JavaScript source collisions", async () => {
    await withTempDir("openclaw-export-collisions-", async (repoRoot) => {
      const sourceRoot = path.join(repoRoot, "src");
      await fs.mkdir(sourceRoot);
      await Promise.all([
        fs.writeFile(path.join(sourceRoot, "alpha.js"), "export const sharedValue = 1;\n"),
        fs.writeFile(path.join(sourceRoot, "beta.mjs"), "export const sharedValue = 2;\n"),
      ]);
      expect(await collectRepositoryCollisions(repoRoot)).toEqual([
        { name: "sharedValue", files: ["src/alpha.js", "src/beta.mjs"] },
      ]);
    });
  });

  it("deduplicates overloads inside one module", () => {
    expect(
      findExportNameCollisions([
        {
          path: "src/overloads.ts",
          content: `
            export function convert(value: string): string;
            export function convert(value: number): number;
            export function convert(value: string | number) { return value; }
          `,
        },
      ]),
    ).toEqual([]);
  });

  it("marks collisions exposed by a Plugin SDK module", () => {
    expect(
      findExportNameCollisions([
        { path: "src/one.ts", content: "export const publicCollision = 1;" },
        { path: "src/two.ts", content: "export function publicCollision() {}" },
        {
          path: "src/plugin-sdk/public.ts",
          content: 'export * from "./public-star.js";',
        },
        {
          path: "src/plugin-sdk/public-star.ts",
          content: 'export * from "../../packages/public.js";',
        },
        {
          path: "packages/public.ts",
          content: "export const publicCollision = true;",
          includeDefinitions: false,
        },
      ]),
    ).toEqual([
      {
        name: "publicCollision",
        files: ["src/one.ts", "src/two.ts"],
        sdk: true,
      },
    ]);
  });
});

describe("export name collision debt baseline", () => {
  it("separates new debt from baseline improvements", () => {
    expect(
      compareExportNameCollisionDebt(
        [
          { name: "added", files: ["src/a.ts", "src/b.ts"] },
          { name: "expanded", files: ["src/a.ts", "src/b.ts", "src/c.ts"], sdk: true },
        ],
        [
          { name: "expanded", files: ["src/a.ts", "src/b.ts"] },
          { name: "removed", files: ["src/c.ts", "src/d.ts"] },
        ],
      ),
    ).toEqual({
      regressions: [
        { current: { name: "added", files: ["src/a.ts", "src/b.ts"] } },
        {
          baseline: { name: "expanded", files: ["src/a.ts", "src/b.ts"] },
          current: {
            name: "expanded",
            files: ["src/a.ts", "src/b.ts", "src/c.ts"],
            sdk: true,
          },
        },
      ],
      improvements: [{ baseline: { name: "removed", files: ["src/c.ts", "src/d.ts"] } }],
    });
  });
});
