import { describe, it, expect } from "vitest";
import type { Node, Edge } from "@xyflow/react";
import { computeSequenceLayout } from "./sequence-layout";
import { SEQ_LAYOUT, SEQ_SIZES } from "../types/sequence/constants";

function createParticipant(id: string, label: string): Node {
  return {
    id,
    type: "seqParticipant",
    position: { x: 0, y: 0 },
    data: { id, type: "seq:participant", label, lifeline: true },
  };
}

function createFragment(id: string, label: string, group: string, tag?: string): Node {
  return {
    id,
    type: "seqFragment",
    position: { x: 0, y: 0 },
    data: { id, type: "seq:fragment", label, group, ...(tag ? { tag } : {}) },
  };
}

function createMessage(id: string, source: string, target: string, type = "seqSync"): Edge {
  return {
    id,
    source,
    target,
    type,
    data: { id, from: source, to: target, type: type === "seqSync" ? "sync" : type === "seqAsync" ? "async" : type === "seqReturn" ? "return" : "sync" },
  };
}

describe("computeSequenceLayout", () => {
  it("should position participants horizontally with correct spacing", () => {
    const nodes = [
      createParticipant("client", "Client"),
      createParticipant("server", "Server"),
      createParticipant("db", "Database"),
    ];
    const result = computeSequenceLayout(nodes, []);

    expect(result.nodes[0]!.position).toEqual({ x: 0, y: 0 });
    expect(result.nodes[1]!.position).toEqual({ x: SEQ_LAYOUT.participantSpacing, y: 0 });
    expect(result.nodes[2]!.position).toEqual({ x: 2 * SEQ_LAYOUT.participantSpacing, y: 0 });
  });

  it("should compute lifeline height from message count", () => {
    const nodes = [createParticipant("a", "A"), createParticipant("b", "B")];
    const edges = [
      createMessage("m1", "a", "b"),
      createMessage("m2", "b", "a"),
      createMessage("m3", "a", "b"),
    ];
    const result = computeSequenceLayout(nodes, edges);

    const expectedHeight =
      SEQ_LAYOUT.messageStartY + 3 * SEQ_LAYOUT.messageSpacing + SEQ_LAYOUT.lifelinePadding;
    const participant = result.nodes[0]!;
    expect((participant.data as Record<string, unknown>).lifelineHeight).toBe(expectedHeight);
    expect(participant.style).toEqual(expect.objectContaining({ height: expectedHeight }));
  });

  it("should enrich edges with order and messageY", () => {
    const nodes = [createParticipant("a", "A"), createParticipant("b", "B")];
    const edges = [
      createMessage("m1", "a", "b"),
      createMessage("m2", "b", "a"),
    ];
    const result = computeSequenceLayout(nodes, edges);

    const e0 = result.edges[0]!.data as Record<string, unknown>;
    expect(e0.order).toBe(0);
    expect(e0.messageY).toBe(SEQ_LAYOUT.messageStartY);

    const e1 = result.edges[1]!.data as Record<string, unknown>;
    expect(e1.order).toBe(1);
    expect(e1.messageY).toBe(SEQ_LAYOUT.messageStartY + SEQ_LAYOUT.messageSpacing);
  });

  it("should position fragment around its messages", () => {
    const nodes = [
      createParticipant("client", "Client"),
      createParticipant("server", "Server"),
      createFragment("alt-1", "alt", "m2,m3", "[valid]"),
    ];
    const edges = [
      createMessage("m1", "client", "server"),
      createMessage("m2", "server", "client"),
      createMessage("m3", "client", "server"),
    ];
    const result = computeSequenceLayout(nodes, edges);

    // Fragment should encompass messages m2 (index 1) and m3 (index 2)
    const frag = result.nodes.find((n) => n.id === "alt-1")!;
    const expectedTopY =
      SEQ_LAYOUT.messageStartY + 1 * SEQ_LAYOUT.messageSpacing - SEQ_LAYOUT.fragmentPadding;
    const expectedBottomY =
      SEQ_LAYOUT.messageStartY + 2 * SEQ_LAYOUT.messageSpacing + SEQ_LAYOUT.fragmentPadding;

    expect(frag.position.y).toBe(expectedTopY);
    expect((frag.style as { height: number }).height).toBe(expectedBottomY - expectedTopY);

    // Fragment X spans from leftmost to rightmost participant + width + padding
    const leftX = 0 - SEQ_LAYOUT.fragmentPadding;
    const rightX = SEQ_LAYOUT.participantSpacing + SEQ_SIZES.participant.w + SEQ_LAYOUT.fragmentPadding;
    expect(frag.position.x).toBe(leftX);
    expect((frag.style as { width: number }).width).toBe(rightX - leftX);
  });

  it("should handle empty edges", () => {
    const nodes = [createParticipant("a", "A")];
    const result = computeSequenceLayout(nodes, []);

    expect(result.nodes).toHaveLength(1);
    expect(result.edges).toHaveLength(0);
    const expectedHeight =
      SEQ_LAYOUT.messageStartY + 0 * SEQ_LAYOUT.messageSpacing + SEQ_LAYOUT.lifelinePadding;
    expect((result.nodes[0]!.data as Record<string, unknown>).lifelineHeight).toBe(expectedHeight);
  });

  it("should handle fragment with no matching message IDs", () => {
    const nodes = [
      createParticipant("a", "A"),
      createFragment("frag-1", "loop", "nonexistent"),
    ];
    const edges = [createMessage("m1", "a", "a")];
    const result = computeSequenceLayout(nodes, edges);

    // Fragment with no matching messages should keep original position
    const frag = result.nodes.find((n) => n.id === "frag-1")!;
    expect(frag.position).toEqual({ x: 0, y: 0 });
  });

  it("should return participants before fragments in node order", () => {
    const nodes = [
      createParticipant("a", "A"),
      createFragment("f1", "opt", "m1"),
      createParticipant("b", "B"),
    ];
    const edges = [createMessage("m1", "a", "b")];
    const result = computeSequenceLayout(nodes, edges);

    // Participants come first, then fragments
    expect(result.nodes[0]!.type).toBe("seqParticipant");
    expect(result.nodes[1]!.type).toBe("seqParticipant");
    expect(result.nodes[2]!.type).toBe("seqFragment");
  });

  it("should NOT mutate input nodes", () => {
    const nodes = [
      createParticipant("a", "A"),
      createParticipant("b", "B"),
    ];
    const originalPos0 = { ...nodes[0]!.position };
    const originalPos1 = { ...nodes[1]!.position };

    computeSequenceLayout(nodes, [createMessage("m1", "a", "b")]);

    // Original nodes should be untouched
    expect(nodes[0]!.position).toEqual(originalPos0);
    expect(nodes[1]!.position).toEqual(originalPos1);
    expect((nodes[0]!.data as Record<string, unknown>).lifelineHeight).toBeUndefined();
  });

  it("should NOT mutate input edges", () => {
    const edges = [createMessage("m1", "a", "b")];
    const originalData = { ...edges[0]!.data };

    computeSequenceLayout(
      [createParticipant("a", "A"), createParticipant("b", "B")],
      edges,
    );

    // Original edge data should not have order/messageY
    expect((edges[0]!.data as Record<string, unknown>).order).toBeUndefined();
    expect((edges[0]!.data as Record<string, unknown>).messageY).toBeUndefined();
  });

  it("should compute activation bars for sync/return message pairs", () => {
    const nodes = [
      createParticipant("client", "Client"),
      createParticipant("server", "Server"),
    ];
    // sync from client→server (opens activation on server), return from server→client (closes it)
    const edges = [
      createMessage("m1", "client", "server", "seqSync"),
      createMessage("m2", "server", "client", "seqReturn"),
    ];
    const result = computeSequenceLayout(nodes, edges);

    // Server should have one activation bar
    const server = result.nodes.find((n) => n.id === "server")!;
    const serverData = server.data as Record<string, unknown>;
    const activations = serverData.activations as { y: number; height: number }[];
    expect(activations).toHaveLength(1);

    // Activation starts at m1's Y, ends at m2's Y
    const m1Y = SEQ_LAYOUT.messageStartY + 0 * SEQ_LAYOUT.messageSpacing;
    const m2Y = SEQ_LAYOUT.messageStartY + 1 * SEQ_LAYOUT.messageSpacing;
    expect(activations[0]!.y).toBe(m1Y);
    expect(activations[0]!.height).toBe(m2Y - m1Y);
  });

  it("should close open activations at lifeline end", () => {
    const nodes = [
      createParticipant("client", "Client"),
      createParticipant("server", "Server"),
    ];
    // sync without a matching return — activation stays open
    const edges = [
      createMessage("m1", "client", "server", "seqSync"),
    ];
    const result = computeSequenceLayout(nodes, edges);

    const server = result.nodes.find((n) => n.id === "server")!;
    const activations = (server.data as Record<string, unknown>).activations as { y: number; height: number }[];
    expect(activations).toHaveLength(1);
    // Should extend to near the end of the lifeline
    expect(activations[0]!.y).toBe(SEQ_LAYOUT.messageStartY);
    const lifelineEnd =
      SEQ_LAYOUT.messageStartY +
      edges.length * SEQ_LAYOUT.messageSpacing; // lifelineHeight - padding
    expect(activations[0]!.height).toBe(lifelineEnd - SEQ_LAYOUT.messageStartY);
  });

  it("should have empty activations for participants with no incoming sync messages", () => {
    const nodes = [
      createParticipant("a", "A"),
      createParticipant("b", "B"),
    ];
    const edges: Edge[] = [];
    const result = computeSequenceLayout(nodes, edges);

    for (const node of result.nodes) {
      if (node.type === "seqParticipant") {
        const activations = (node.data as Record<string, unknown>).activations as { y: number; height: number }[];
        expect(activations).toEqual([]);
      }
    }
  });
});
