// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { loadPathAliases, parsePathAliases } from "../../src/services/graph-aliases.js";

describe("parsePathAliases", () => {
  const projectPath = "/project";

  it("parses tsconfig paths with wildcard patterns", () => {
    const json = JSON.stringify({
      compilerOptions: {
        paths: {
          "$lib/*": ["src/lib/*"],
          "@/*": ["./src/*"],
        },
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });

  it("respects baseUrl", () => {
    const json = JSON.stringify({
      compilerOptions: {
        baseUrl: "./app",
        paths: {
          "@/*": ["src/*"],
        },
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("@/")).toEqual([path.join("app", "src")]);
  });

  it("handles exact (non-wildcard) patterns", () => {
    const json = JSON.stringify({
      compilerOptions: {
        paths: {
          "~": ["./src"],
        },
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("~")).toEqual(["src"]);
  });

  it("handles multiple targets for a single alias", () => {
    const json = JSON.stringify({
      compilerOptions: {
        paths: {
          "@/*": ["src/*", "generated/*"],
        },
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("@/")).toEqual(["src", "generated"]);
  });

  it("handles JSON with single-line comments", () => {
    const json = `{
      // This is a comment
      "compilerOptions": {
        "paths": {
          "$lib/*": ["src/lib/*"] // inline comment
        }
      }
    }`;

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
  });

  it("handles JSON with multi-line comments", () => {
    const json = `{
      /* Multi-line
         comment */
      "compilerOptions": {
        "paths": {
          "@/*": ["src/*"]
        }
      }
    }`;

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });

  it("preserves // inside string values when stripping comments", () => {
    const json = `{
      // comment
      "compilerOptions": {
        "baseUrl": "./dist//subdir", // trailing comment
        "paths": {
          "@/*": ["src/*"]
        }
      }
    }`;

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("@/")).toEqual([path.join("dist//subdir", "src")]);
  });

  it("returns empty entries for config without paths", () => {
    const json = JSON.stringify({
      compilerOptions: {
        strict: true,
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.size).toBe(0);
  });

  it("returns empty entries for invalid JSON", () => {
    const aliases = parsePathAliases("not json at all", projectPath);

    expect(aliases.entries.size).toBe(0);
  });

  it("skips entries with empty targets array", () => {
    const json = JSON.stringify({
      compilerOptions: {
        paths: {
          "@/*": [],
          "$lib/*": ["src/lib/*"],
        },
      },
    });

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.has("@/")).toBe(false);
    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
  });

  it("skips non-string targets gracefully", () => {
    const json = '{"compilerOptions":{"paths":{"@/*":[42, null, "./src/*"]}}}';

    const aliases = parsePathAliases(json, projectPath);

    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });
});

describe("loadPathAliases", () => {
  let tmpDir: string | null = null;

  afterEach(() => {
    if (tmpDir) {
      try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
      tmpDir = null;
    }
  });

  it("returns empty aliases for non-existent project", async () => {
    const aliases = await loadPathAliases("/non/existent/path");

    expect(aliases.entries.size).toBe(0);
  });

  it("reads aliases from tsconfig.json", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      compilerOptions: {
        paths: { "$lib/*": ["src/lib/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
  });

  it("falls back to jsconfig.json when tsconfig.json is absent", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "jsconfig.json"), JSON.stringify({
      compilerOptions: {
        paths: { "@/*": ["./src/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });

  it("falls through to jsconfig.json when tsconfig.json has no paths", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      compilerOptions: { strict: true },
    }));
    fs.writeFileSync(path.join(tmpDir, "jsconfig.json"), JSON.stringify({
      compilerOptions: {
        paths: { "@/*": ["./src/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });

  it("prefers tsconfig.json over jsconfig.json", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      compilerOptions: {
        paths: { "$lib/*": ["src/lib/*"] },
      },
    }));
    fs.writeFileSync(path.join(tmpDir, "jsconfig.json"), JSON.stringify({
      compilerOptions: {
        paths: { "@/*": ["./src/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    // Should use tsconfig.json paths, not jsconfig.json
    expect(aliases.entries.has("$lib/")).toBe(true);
    expect(aliases.entries.has("@/")).toBe(false);
  });

  it("follows extends chain to find paths in parent config", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      extends: "./tsconfig.base.json",
      compilerOptions: { strict: true },
    }));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.base.json"), JSON.stringify({
      compilerOptions: {
        paths: { "$lib/*": ["src/lib/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
  });

  it("follows multi-level extends chain", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      extends: "./tsconfig.app.json",
    }));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.app.json"), JSON.stringify({
      extends: "./tsconfig.base.json",
      compilerOptions: { strict: true },
    }));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.base.json"), JSON.stringify({
      compilerOptions: {
        paths: { "@/*": ["src/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("@/")).toEqual(["src"]);
  });

  it("handles extends without .json extension", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      extends: "./base",
    }));
    fs.writeFileSync(path.join(tmpDir, "base.json"), JSON.stringify({
      compilerOptions: {
        paths: { "$lib/*": ["src/lib/*"] },
      },
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.get("$lib/")).toEqual(["src/lib"]);
  });

  it("handles circular extends gracefully", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      extends: "./tsconfig.base.json",
    }));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.base.json"), JSON.stringify({
      extends: "./tsconfig.json",
    }));

    const aliases = await loadPathAliases(tmpDir);

    // Should not infinite loop, returns empty
    expect(aliases.entries.size).toBe(0);
  });

  it("stops when extended file does not exist", async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "socraticode-aliases-"));
    fs.writeFileSync(path.join(tmpDir, "tsconfig.json"), JSON.stringify({
      extends: "./nonexistent.json",
    }));

    const aliases = await loadPathAliases(tmpDir);

    expect(aliases.entries.size).toBe(0);
  });
});
