// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { describe, expect, it } from "vitest";
import {
  findCircularDependencies,
  generateMermaidDiagram,
  getFileDependencies,
  getGraphStats,
} from "../../src/services/graph-analysis.js";
import type { CodeGraph, CodeGraphEdge, CodeGraphNode } from "../../src/types.js";

// ── Helper to build mock graphs ────────────────────────────────────────

function makeNode(
  relativePath: string,
  deps: string[] = [],
  dependents: string[] = [],
): CodeGraphNode {
  return {
    filePath: `/project/${relativePath}`,
    relativePath,
    imports: deps.map((d) => `./${d}`),
    exports: [],
    dependencies: deps,
    dependents,
  };
}

function makeEdge(
  source: string,
  target: string,
  type: "import" | "re-export" | "dynamic-import" = "import",
): CodeGraphEdge {
  return { source, target, type };
}

function makeGraph(nodes: CodeGraphNode[], edges: CodeGraphEdge[]): CodeGraph {
  return { nodes, edges };
}

// ── A realistic small graph ──────────────────────────────────────────────
// index.ts → utils.ts → helpers.ts
// index.ts → types.ts
// types.ts (orphan-ish: no imports)

function createSampleGraph(): CodeGraph {
  const nodes: CodeGraphNode[] = [
    makeNode("src/index.ts", ["src/utils.ts", "src/types.ts"], []),
    makeNode("src/utils.ts", ["src/helpers.ts"], ["src/index.ts"]),
    makeNode("src/helpers.ts", [], ["src/utils.ts"]),
    makeNode("src/types.ts", [], ["src/index.ts"]),
  ];

  const edges: CodeGraphEdge[] = [
    makeEdge("src/index.ts", "src/utils.ts"),
    makeEdge("src/index.ts", "src/types.ts"),
    makeEdge("src/utils.ts", "src/helpers.ts"),
  ];

  return makeGraph(nodes, edges);
}

// ── A graph with circular dependencies ────────────────────────────────

function createCircularGraph(): CodeGraph {
  // A → B → C → A (cycle)
  const nodes: CodeGraphNode[] = [
    makeNode("a.ts", ["b.ts"], ["c.ts"]),
    makeNode("b.ts", ["c.ts"], ["a.ts"]),
    makeNode("c.ts", ["a.ts"], ["b.ts"]),
  ];

  const edges: CodeGraphEdge[] = [
    makeEdge("a.ts", "b.ts"),
    makeEdge("b.ts", "c.ts"),
    makeEdge("c.ts", "a.ts"),
  ];

  return makeGraph(nodes, edges);
}

describe("graph-analysis", () => {
  describe("getFileDependencies", () => {
    it("returns imports and importedBy for a file", () => {
      const graph = createSampleGraph();
      const deps = getFileDependencies(graph, "src/index.ts");

      expect(deps.imports).toContain("src/utils.ts");
      expect(deps.imports).toContain("src/types.ts");
      expect(deps.importedBy).toHaveLength(0); // nothing imports index.ts
    });

    it("returns dependents correctly", () => {
      const graph = createSampleGraph();
      const deps = getFileDependencies(graph, "src/utils.ts");

      expect(deps.imports).toContain("src/helpers.ts");
      expect(deps.importedBy).toContain("src/index.ts");
    });

    it("returns empty arrays for leaf files with no dependents", () => {
      const graph = createSampleGraph();
      const deps = getFileDependencies(graph, "src/helpers.ts");

      expect(deps.imports).toHaveLength(0);
      expect(deps.importedBy).toContain("src/utils.ts");
    });

    it("returns empty arrays for non-existent file", () => {
      const graph = createSampleGraph();
      const deps = getFileDependencies(graph, "nonexistent.ts");

      expect(deps.imports).toHaveLength(0);
      expect(deps.importedBy).toHaveLength(0);
    });
  });

  describe("findCircularDependencies", () => {
    it("returns empty array when no cycles exist", () => {
      const graph = createSampleGraph();
      const cycles = findCircularDependencies(graph);
      expect(cycles).toHaveLength(0);
    });

    it("detects circular dependencies", () => {
      const graph = createCircularGraph();
      const cycles = findCircularDependencies(graph);
      expect(cycles.length).toBeGreaterThan(0);
    });

    it("cycle path starts and ends with the same file", () => {
      const graph = createCircularGraph();
      const cycles = findCircularDependencies(graph);
      for (const cycle of cycles) {
        expect(cycle[0]).toBe(cycle[cycle.length - 1]);
      }
    });

    it("handles empty graph", () => {
      const graph = makeGraph([], []);
      const cycles = findCircularDependencies(graph);
      expect(cycles).toHaveLength(0);
    });

    it("handles single-node graph", () => {
      const graph = makeGraph([makeNode("solo.ts")], []);
      const cycles = findCircularDependencies(graph);
      expect(cycles).toHaveLength(0);
    });

    it("detects self-import cycle", () => {
      const node = makeNode("self.ts", ["self.ts"], ["self.ts"]);
      const graph = makeGraph([node], [makeEdge("self.ts", "self.ts")]);
      const cycles = findCircularDependencies(graph);
      expect(cycles.length).toBeGreaterThan(0);
    });
  });

  describe("getGraphStats", () => {
    it("returns correct total files", () => {
      const graph = createSampleGraph();
      const stats = getGraphStats(graph);
      expect(stats.totalFiles).toBe(4);
    });

    it("returns correct total edges", () => {
      const graph = createSampleGraph();
      const stats = getGraphStats(graph);
      expect(stats.totalEdges).toBe(3);
    });

    it("calculates average dependencies", () => {
      const graph = createSampleGraph();
      const stats = getGraphStats(graph);
      expect(stats.avgDependencies).toBe(3 / 4);
    });

    it("identifies most connected files", () => {
      const graph = createSampleGraph();
      const stats = getGraphStats(graph);
      expect(stats.mostConnected.length).toBeGreaterThan(0);
      // index.ts has 2 deps + 0 dependents = 2 connections
      // utils.ts has 1 dep + 1 dependent = 2 connections
      const indexEntry = stats.mostConnected.find((f) => f.file === "src/index.ts");
      expect(indexEntry).toBeDefined();
      expect(indexEntry?.connections).toBe(2);
    });

    it("identifies orphan files (no deps and no dependents)", () => {
      // Add a true orphan to the graph
      const graph = createSampleGraph();
      graph.nodes.push(makeNode("orphan.ts"));
      const stats = getGraphStats(graph);
      expect(stats.orphans).toContain("orphan.ts");
    });

    it("counts circular dependencies", () => {
      const graph = createCircularGraph();
      const stats = getGraphStats(graph);
      expect(stats.circularDeps).toBeGreaterThan(0);
    });

    it("returns language breakdown", () => {
      const graph = createSampleGraph();
      const stats = getGraphStats(graph);
      expect(stats.languageBreakdown).toBeDefined();
      expect(stats.languageBreakdown.typescript).toBe(4);
    });

    it("handles empty graph", () => {
      const graph = makeGraph([], []);
      const stats = getGraphStats(graph);
      expect(stats.totalFiles).toBe(0);
      expect(stats.totalEdges).toBe(0);
      expect(stats.avgDependencies).toBe(0);
      expect(stats.mostConnected).toHaveLength(0);
      expect(stats.orphans).toHaveLength(0);
      expect(stats.circularDeps).toBe(0);
    });

    it("limits mostConnected to top 10", () => {
      // Create a graph with more than 10 nodes
      const nodes: CodeGraphNode[] = [];
      const edges: CodeGraphEdge[] = [];
      for (let i = 0; i < 15; i++) {
        nodes.push(makeNode(`file${i}.ts`));
      }
      const graph = makeGraph(nodes, edges);
      const stats = getGraphStats(graph);
      expect(stats.mostConnected.length).toBeLessThanOrEqual(10);
    });
  });

  describe("generateMermaidDiagram", () => {
    it("generates a valid Mermaid diagram", () => {
      const graph = createSampleGraph();
      const mermaid = generateMermaidDiagram(graph);

      expect(mermaid).toContain("graph LR");
    });

    it("includes node declarations with file basenames", () => {
      const graph = createSampleGraph();
      const mermaid = generateMermaidDiagram(graph);

      expect(mermaid).toContain('"index.ts"');
      expect(mermaid).toContain('"utils.ts"');
      expect(mermaid).toContain('"helpers.ts"');
      expect(mermaid).toContain('"types.ts"');
    });

    it("includes edge arrows", () => {
      const graph = createSampleGraph();
      const mermaid = generateMermaidDiagram(graph);

      // Should have arrow notation
      expect(mermaid).toContain("-->");
    });

    it("handles empty graph", () => {
      const graph = makeGraph([], []);
      const mermaid = generateMermaidDiagram(graph);
      expect(mermaid).toContain("No files found");
    });

    it("marks circular dependency edges specially", () => {
      const graph = createCircularGraph();
      const mermaid = generateMermaidDiagram(graph);

      // Circular edges should use dotted lines with |cycle| label
      expect(mermaid).toContain("-.->|cycle|");
    });

    it("includes style directives with colors", () => {
      const graph = createSampleGraph();
      const mermaid = generateMermaidDiagram(graph);

      // TypeScript nodes should have the TypeScript color fill
      expect(mermaid).toContain("style");
      expect(mermaid).toContain("fill:");
    });

    it("handles dynamic-import edges with dotted lines", () => {
      const graph = makeGraph(
        [makeNode("a.ts", ["b.ts"], []), makeNode("b.ts", [], ["a.ts"])],
        [makeEdge("a.ts", "b.ts", "dynamic-import")],
      );
      const mermaid = generateMermaidDiagram(graph);
      // Dynamic imports use dotted lines (-.->)
      expect(mermaid).toContain("-.->"); 
    });

    it("includes a legend for multi-language graphs", () => {
      const nodes: CodeGraphNode[] = [
        makeNode("app.ts", [], []),
        makeNode("lib.py", [], []),
      ];
      // Fix the filePath to have proper extension recognition
      nodes[1].filePath = "/project/lib.py";

      const graph = makeGraph(nodes, []);
      const mermaid = generateMermaidDiagram(graph);
      expect(mermaid).toContain("subgraph Legend");
    });
  });
});
