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 compute E-R entity height from columns only for erEntity nodes", () => {
    const erNode: Node = {
      id: "users",
      type: "erEntity",
      position: { x: 0, y: 0 },
      data: {
        id: "users",
        type: "er:entity",
        label: "Users",
        columns: [
          { name: "id", type: "uuid", isPrimaryKey: true },
          { name: "name", type: "text" },
          { name: "email", type: "varchar" },
        ],
      },
    };
    const regularNode = createNode("n1");
    const result = buildElkGraph([erNode, regularNode], []);

    // E-R entity: headerH(36) + 3*rowH(24) + paddingY(8)*2 = 124
    expect(result.children?.[0]?.height).toBe(124);
    // Regular node: default height (50)
    expect(result.children?.[1]?.height).toBe(50);
  });

  it("should build correct ELK graph for M:N junction table scenario (AC #3)", () => {
    const students: Node = {
      id: "students",
      type: "erEntity",
      position: { x: 0, y: 0 },
      data: {
        id: "students",
        type: "er:entity",
        label: "Students",
        columns: [
          { name: "id", type: "uuid", isPrimaryKey: true },
          { name: "name", type: "text" },
        ],
      },
    };
    const courses: Node = {
      id: "courses",
      type: "erEntity",
      position: { x: 0, y: 0 },
      data: {
        id: "courses",
        type: "er:entity",
        label: "Courses",
        columns: [
          { name: "id", type: "uuid", isPrimaryKey: true },
          { name: "title", type: "text" },
        ],
      },
    };
    const enrollments: Node = {
      id: "enrollments",
      type: "erEntity",
      position: { x: 0, y: 0 },
      data: {
        id: "enrollments",
        type: "er:entity",
        label: "Enrollments",
        columns: [
          { name: "student_id", type: "uuid", isForeignKey: true },
          { name: "course_id", type: "uuid", isForeignKey: true },
        ],
      },
    };
    const edges: Edge[] = [
      { id: "e1", source: "students", target: "enrollments", type: "erRelationship", data: {} },
      { id: "e2", source: "courses", target: "enrollments", type: "erRelationship", data: {} },
    ];

    const result = buildElkGraph([students, courses, enrollments], edges);

    // All 3 entities as flat children (no hierarchy)
    expect(result.children).toHaveLength(3);
    // Junction table has correct height: headerH(36) + 2*rowH(24) + paddingY(8)*2 = 100
    const junctionChild = result.children?.find((c) => c.id === "enrollments");
    expect(junctionChild?.height).toBe(100);
    // 2 edges connecting to junction table
    expect(result.edges).toHaveLength(2);
    // No nested children (flat layout, not compound)
    expect(result.children?.every((c) => !("children" in c && (c as { children?: unknown[] }).children?.length))).toBe(true);
  });

  it("should NOT use E-R height for non-erEntity nodes with columns property", () => {
    const nodeWithColumns: Node = {
      id: "n1",
      type: "default",
      position: { x: 0, y: 0 },
      data: {
        id: "n1",
        type: "flow:process",
        label: "Not E-R",
        columns: [{ name: "a", type: "text" }],
      },
    };
    const result = buildElkGraph([nodeWithColumns], []);
    // Should use default height (50), NOT E-R computed height
    expect(result.children?.[0]?.height).toBe(50);
  });

  it("should use OC_SIZES for orgchartPerson nodes", () => {
    const ocNode: Node = {
      id: "ceo",
      type: "orgchartPerson",
      position: { x: 0, y: 0 },
      data: {
        id: "ceo",
        type: "org:person",
        label: "Alice",
        tag: "CEO",
      },
    };
    const regularNode = createNode("n1");
    const result = buildElkGraph([ocNode, regularNode], []);

    // Org chart person: fixed w=280, h=80
    expect(result.children?.[0]?.width).toBe(280);
    expect(result.children?.[0]?.height).toBe(80);
    // Regular node: default dimensions
    expect(result.children?.[1]?.width).toBe(150);
    expect(result.children?.[1]?.height).toBe(50);
  });

  it("should respect data.w override for orgchartPerson nodes", () => {
    const ocNode: Node = {
      id: "ceo",
      type: "orgchartPerson",
      position: { x: 0, y: 0 },
      data: {
        id: "ceo",
        type: "org:person",
        label: "Alice",
        tag: "CEO",
        w: 320,
      },
    };
    const result = buildElkGraph([ocNode], []);
    expect(result.children?.[0]?.width).toBe(320);
    expect(result.children?.[0]?.height).toBe(80);
  });

  it("should use ARCH_SIZES for architecture node subtypes", () => {
    const archNodes: Node[] = [
      {
        id: "api",
        type: "archService",
        position: { x: 0, y: 0 },
        data: { id: "api", type: "arch:service", label: "API Gateway" },
      },
      {
        id: "db",
        type: "archDatabase",
        position: { x: 0, y: 0 },
        data: { id: "db", type: "arch:database", label: "PostgreSQL" },
      },
      {
        id: "q",
        type: "archQueue",
        position: { x: 0, y: 0 },
        data: { id: "q", type: "arch:queue", label: "SQS" },
      },
      {
        id: "lb",
        type: "archLoadBalancer",
        position: { x: 0, y: 0 },
        data: { id: "lb", type: "arch:loadbalancer", label: "ALB" },
      },
      {
        id: "ext",
        type: "archExternal",
        position: { x: 0, y: 0 },
        data: { id: "ext", type: "arch:external", label: "Stripe" },
      },
    ];

    const result = buildElkGraph(archNodes, []);

    // archService: w=200, h=80
    expect(result.children?.[0]?.width).toBe(200);
    expect(result.children?.[0]?.height).toBe(80);
    // archDatabase: w=160, h=100
    expect(result.children?.[1]?.width).toBe(160);
    expect(result.children?.[1]?.height).toBe(100);
    // archQueue: w=180, h=70
    expect(result.children?.[2]?.width).toBe(180);
    expect(result.children?.[2]?.height).toBe(70);
    // archLoadBalancer: w=120, h=120
    expect(result.children?.[3]?.width).toBe(120);
    expect(result.children?.[3]?.height).toBe(120);
    // archExternal: w=180, h=80
    expect(result.children?.[4]?.width).toBe(180);
    expect(result.children?.[4]?.height).toBe(80);
  });

  it("should respect data.w override for architecture nodes", () => {
    const archNode: Node = {
      id: "api",
      type: "archService",
      position: { x: 0, y: 0 },
      data: { id: "api", type: "arch:service", label: "Wide Service", w: 300 },
    };
    const result = buildElkGraph([archNode], []);
    expect(result.children?.[0]?.width).toBe(300);
    expect(result.children?.[0]?.height).toBe(80);
  });

  it("should use SEQ_SIZES for seqParticipant nodes", () => {
    const seqNode: Node = {
      id: "client",
      type: "seqParticipant",
      position: { x: 0, y: 0 },
      data: { id: "client", type: "seq:participant", label: "Client", lifeline: true },
    };
    const regularNode = createNode("n1");
    const result = buildElkGraph([seqNode, regularNode], []);

    // seqParticipant: w=160, h=60
    expect(result.children?.[0]?.width).toBe(160);
    expect(result.children?.[0]?.height).toBe(60);
    // Regular node: default dimensions
    expect(result.children?.[1]?.width).toBe(150);
    expect(result.children?.[1]?.height).toBe(50);
  });

  it("should return null size for seqFragment (dynamically computed)", () => {
    const fragNode: Node = {
      id: "alt-1",
      type: "seqFragment",
      position: { x: 0, y: 0 },
      data: { id: "alt-1", type: "seq:fragment", label: "alt" },
    };
    const result = buildElkGraph([fragNode], []);

    // seqFragment returns null from getSeqNodeSize → falls through to default
    expect(result.children?.[0]?.width).toBe(150);
    expect(result.children?.[0]?.height).toBe(50);
  });

  it("should use FLOW_SIZES for flowchart node subtypes", () => {
    const flowNodes: Node[] = [
      {
        id: "step1",
        type: "flowProcess",
        position: { x: 0, y: 0 },
        data: { id: "step1", type: "flow:process", label: "Step 1" },
      },
      {
        id: "check1",
        type: "flowDecision",
        position: { x: 0, y: 0 },
        data: { id: "check1", type: "flow:decision", label: "OK?" },
      },
      {
        id: "start",
        type: "flowTerminal",
        position: { x: 0, y: 0 },
        data: { id: "start", type: "flow:terminal", label: "Start", tag: "start" },
      },
      {
        id: "input1",
        type: "flowIo",
        position: { x: 0, y: 0 },
        data: { id: "input1", type: "flow:io", label: "Read Input" },
      },
      {
        id: "sub1",
        type: "flowSubprocess",
        position: { x: 0, y: 0 },
        data: { id: "sub1", type: "flow:subprocess", label: "Validate" },
      },
    ];

    const result = buildElkGraph(flowNodes, []);

    // flowProcess: w=160, h=60
    expect(result.children?.[0]?.width).toBe(160);
    expect(result.children?.[0]?.height).toBe(60);
    // flowDecision: w=140, h=130
    expect(result.children?.[1]?.width).toBe(140);
    expect(result.children?.[1]?.height).toBe(130);
    // flowTerminal: w=140, h=50
    expect(result.children?.[2]?.width).toBe(140);
    expect(result.children?.[2]?.height).toBe(50);
    // flowIo: w=160, h=60
    expect(result.children?.[3]?.width).toBe(160);
    expect(result.children?.[3]?.height).toBe(60);
    // flowSubprocess: w=160, h=60
    expect(result.children?.[4]?.width).toBe(160);
    expect(result.children?.[4]?.height).toBe(60);
  });

  it("should respect data.w override for flowchart nodes", () => {
    const flowNode: Node = {
      id: "step1",
      type: "flowProcess",
      position: { x: 0, y: 0 },
      data: { id: "step1", type: "flow:process", label: "Wide Step", w: 250 },
    };
    const result = buildElkGraph([flowNode], []);
    expect(result.children?.[0]?.width).toBe(250);
    expect(result.children?.[0]?.height).toBe(60);
  });

  it("should NOT use flowchart sizing for non-flowchart types", () => {
    const regularNode = createNode("n1");
    const result = buildElkGraph([regularNode], []);
    // Regular node: default dimensions (150x50), NOT flowchart sizes
    expect(result.children?.[0]?.width).toBe(150);
    expect(result.children?.[0]?.height).toBe(50);
  });

  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);
  });
});
