import { describe, expect, it } from "vitest";

import type { Node, Edge } from "@xyflow/react";

import { buildElkGraph, resolvePositions, SOFT_CAP_NODE_COUNT } from "./elk-layout";

import type { DiagramNode } from "../types/graph";

// ── Test Helpers ────────────────────────────────────────────────────────────

function createNode(
  id: string,
  overrides: Partial<DiagramNode> = {},
): Node {
  return {
    id,
    type: "default",
    position: { x: 0, y: 0 },
    data: {
      id,
      type: "flow:process",
      label: `Node ${id}`,
      ...overrides,
    },
  };
}

function createEdge(id: string, source: string, target: string): Edge {
  return {
    id,
    source,
    target,
    type: "default",
    data: { id, from: source, to: target },
  };
}

// ── buildElkGraph Tests ─────────────────────────────────────────────────────

describe("buildElkGraph", () => {
  it("should create an ELK graph with correct root structure", () => {
    const nodes = [createNode("n1"), createNode("n2")];
    const edges = [createEdge("e1", "n1", "n2")];

    const result = buildElkGraph(nodes, edges);

    expect(result.id).toBe("root");
    expect(result.layoutOptions?.["elk.algorithm"]).toBe("layered");
    expect(result.children).toHaveLength(2);
    expect(result.edges).toHaveLength(1);
  });

  it("should use default layout options when none provided", () => {
    const result = buildElkGraph([createNode("n1")], []);

    expect(result.layoutOptions?.["elk.direction"]).toBe("DOWN");
    expect(result.layoutOptions?.["elk.edgeRouting"]).toBe("ORTHOGONAL");
    expect(result.layoutOptions?.["elk.spacing.nodeNode"]).toBe("80");
    expect(
      result.layoutOptions?.["elk.layered.spacing.nodeNodeBetweenLayers"],
    ).toBe("100");
  });

  it("should apply custom layout direction and edge routing", () => {
    const result = buildElkGraph([createNode("n1")], [], {
      direction: "RIGHT",
      edgeRouting: "SPLINES",
    });

    expect(result.layoutOptions?.["elk.direction"]).toBe("RIGHT");
    expect(result.layoutOptions?.["elk.edgeRouting"]).toBe("SPLINES");
  });

  it("should apply custom spacing options", () => {
    const result = buildElkGraph([createNode("n1")], [], {
      nodeSpacing: 40,
      layerSpacing: 60,
    });

    expect(result.layoutOptions?.["elk.spacing.nodeNode"]).toBe("40");
    expect(
      result.layoutOptions?.["elk.layered.spacing.nodeNodeBetweenLayers"],
    ).toBe("60");
  });

  it("should use node w field for width when available", () => {
    const nodes = [createNode("n1", { w: 200 })];
    const result = buildElkGraph(nodes, []);

    expect(result.children?.[0]?.width).toBe(200);
  });

  it("should use default width (150) when w field is not set", () => {
    const nodes = [createNode("n1")];
    const result = buildElkGraph(nodes, []);

    expect(result.children?.[0]?.width).toBe(150);
  });

  it("should map edges with sources/targets arrays", () => {
    const edges = [createEdge("e1", "n1", "n2")];
    const result = buildElkGraph([createNode("n1"), createNode("n2")], edges);

    const elkEdge = result.edges?.[0] as { sources: string[]; targets: string[] };
    expect(elkEdge.sources).toEqual(["n1"]);
    expect(elkEdge.targets).toEqual(["n2"]);
  });

  it("should handle empty nodes and edges", () => {
    const result = buildElkGraph([], []);

    expect(result.children).toHaveLength(0);
    expect(result.edges).toHaveLength(0);
  });

  it("should support all four layout directions", () => {
    for (const direction of ["DOWN", "RIGHT", "LEFT", "UP"] as const) {
      const result = buildElkGraph([createNode("n1")], [], { direction });
      expect(result.layoutOptions?.["elk.direction"]).toBe(direction);
    }
  });

  it("should support all three edge routing modes", () => {
    for (const edgeRouting of ["ORTHOGONAL", "SPLINES", "POLYLINE"] as const) {
      const result = buildElkGraph([createNode("n1")], [], { edgeRouting });
      expect(result.layoutOptions?.["elk.edgeRouting"]).toBe(edgeRouting);
    }
  });
});

// ── resolvePositions Tests ──────────────────────────────────────────────────

describe("resolvePositions", () => {
  it("should map ELK positions to nodes", () => {
    const originalNodes = [createNode("n1"), createNode("n2")];
    const elkGraph = {
      id: "root",
      children: [
        { id: "n1", x: 100, y: 200, width: 150, height: 50 },
        { id: "n2", x: 300, y: 400, width: 150, height: 50 },
      ],
    };

    const result = resolvePositions(elkGraph, originalNodes);

    expect(result[0]?.position).toEqual({ x: 100, y: 200 });
    expect(result[1]?.position).toEqual({ x: 300, y: 400 });
  });

  it("should skip nodes with manuallyPositioned flag", () => {
    const manualNode = createNode("n1", { manuallyPositioned: true });
    const autoNode = createNode("n2");

    const elkGraph = {
      id: "root",
      children: [
        { id: "n1", x: 100, y: 200, width: 150, height: 50 },
        { id: "n2", x: 300, y: 400, width: 150, height: 50 },
      ],
    };

    const result = resolvePositions(elkGraph, [manualNode, autoNode]);

    // Manual node should keep its original position (0,0 from createNode)
    expect(result[0]?.position).toEqual({ x: 0, y: 0 });
    // Auto node should get ELK position
    expect(result[1]?.position).toEqual({ x: 300, y: 400 });
  });

  it("should NOT skip nodes that have position but no manuallyPositioned flag", () => {
    const nodeWithPosition = createNode("n1", { position: { x: 50, y: 50 } });
    const elkGraph = {
      id: "root",
      children: [
        { id: "n1", x: 100, y: 200, width: 150, height: 50 },
      ],
    };

    const result = resolvePositions(elkGraph, [nodeWithPosition]);

    // Should get ELK position since manuallyPositioned is not set
    expect(result[0]?.position).toEqual({ x: 100, y: 200 });
  });

  it("should preserve node when no matching ELK position exists", () => {
    const originalNodes = [createNode("n1"), createNode("orphan")];
    const elkGraph = {
      id: "root",
      children: [{ id: "n1", x: 100, y: 200, width: 150, height: 50 }],
    };

    const result = resolvePositions(elkGraph, originalNodes);

    expect(result[0]?.position).toEqual({ x: 100, y: 200 });
    expect(result[1]?.position).toEqual({ x: 0, y: 0 }); // unchanged
  });

  it("should handle empty ELK graph", () => {
    const originalNodes = [createNode("n1")];
    const elkGraph = { id: "root", children: [] };

    const result = resolvePositions(elkGraph, originalNodes);

    expect(result[0]?.position).toEqual({ x: 0, y: 0 });
  });

  it("should handle ELK graph with no children property", () => {
    const originalNodes = [createNode("n1")];
    const elkGraph = { id: "root" };

    const result = resolvePositions(elkGraph, originalNodes);

    expect(result[0]?.position).toEqual({ x: 0, y: 0 });
  });

  it("should not mutate original nodes", () => {
    const originalNodes = [createNode("n1")];
    const originalPosition = { ...originalNodes[0]!.position };
    const elkGraph = {
      id: "root",
      children: [{ id: "n1", x: 100, y: 200, width: 150, height: 50 }],
    };

    resolvePositions(elkGraph, originalNodes);

    expect(originalNodes[0]!.position).toEqual(originalPosition);
  });
});

// ── Constants Tests ─────────────────────────────────────────────────────────

describe("SOFT_CAP_NODE_COUNT", () => {
  it("should be 200", () => {
    expect(SOFT_CAP_NODE_COUNT).toBe(200);
  });
});
