import { describe, it, expect, beforeEach } from "vitest";
import { useGraphStore } from "./useGraphStore";

import type { Node, Edge } from "@xyflow/react";
import type { GraphData } from "../types/graph";

const makeNode = (id: string, label = "Node"): Node => ({
  id,
  type: "default",
  position: { x: 0, y: 0 },
  data: { label },
});

const makeEdge = (id: string, source: string, target: string): Edge => ({
  id,
  source,
  target,
});

describe("useGraphStore", () => {
  beforeEach(() => {
    useGraphStore.getState().reset();
  });

  describe("initializeFromGraphData", () => {
    it("should set nodes, edges, and nodeCount", () => {
      const nodes = [makeNode("n1"), makeNode("n2")];
      const edges = [makeEdge("e1", "n1", "n2")];

      useGraphStore.getState().initializeFromGraphData(nodes, edges);

      expect(useGraphStore.getState().nodes).toHaveLength(2);
      expect(useGraphStore.getState().edges).toHaveLength(1);
      expect(useGraphStore.getState().nodeCount).toBe(2);
    });
  });

  describe("setNodes", () => {
    it("should replace nodes and update nodeCount", () => {
      useGraphStore.getState().setNodes([makeNode("a"), makeNode("b"), makeNode("c")]);
      expect(useGraphStore.getState().nodes).toHaveLength(3);
      expect(useGraphStore.getState().nodeCount).toBe(3);
    });

    it("should handle empty array", () => {
      useGraphStore.getState().setNodes([makeNode("x")]);
      useGraphStore.getState().setNodes([]);
      expect(useGraphStore.getState().nodes).toEqual([]);
      expect(useGraphStore.getState().nodeCount).toBe(0);
    });
  });

  describe("setEdges", () => {
    it("should replace edges", () => {
      const edges = [makeEdge("e1", "a", "b"), makeEdge("e2", "b", "c")];
      useGraphStore.getState().setEdges(edges);
      expect(useGraphStore.getState().edges).toHaveLength(2);
    });
  });

  describe("onNodesChange", () => {
    it("should apply node position changes", () => {
      useGraphStore.getState().setNodes([makeNode("n1")]);

      useGraphStore.getState().onNodesChange([
        {
          type: "position",
          id: "n1",
          position: { x: 100, y: 200 },
        },
      ]);

      const updatedNode = useGraphStore.getState().nodes[0];
      expect(updatedNode!.position).toEqual({ x: 100, y: 200 });
    });

    it("should update nodeCount when nodes are removed", () => {
      useGraphStore.getState().setNodes([makeNode("n1"), makeNode("n2")]);
      expect(useGraphStore.getState().nodeCount).toBe(2);

      useGraphStore.getState().onNodesChange([
        { type: "remove", id: "n1" },
      ]);

      expect(useGraphStore.getState().nodeCount).toBe(1);
    });
  });

  describe("onEdgesChange", () => {
    it("should apply edge removal", () => {
      useGraphStore.getState().setEdges([makeEdge("e1", "a", "b")]);

      useGraphStore.getState().onEdgesChange([
        { type: "remove", id: "e1" },
      ]);

      expect(useGraphStore.getState().edges).toHaveLength(0);
    });
  });

  describe("onViewportChange", () => {
    it("should update viewport and zoomLevel", () => {
      useGraphStore.getState().onViewportChange({ x: 50, y: 50, zoom: 1.5 });

      expect(useGraphStore.getState().viewport).toEqual({ x: 50, y: 50, zoom: 1.5 });
      expect(useGraphStore.getState().zoomLevel).toBe(150);
    });

    it("should round zoomLevel to nearest integer", () => {
      useGraphStore.getState().onViewportChange({ x: 0, y: 0, zoom: 0.333 });
      expect(useGraphStore.getState().zoomLevel).toBe(33);
    });

    it("should handle zoom at 100%", () => {
      useGraphStore.getState().onViewportChange({ x: 0, y: 0, zoom: 1.0 });
      expect(useGraphStore.getState().zoomLevel).toBe(100);
    });
  });

  describe("layout state", () => {
    it("should have default layout direction DOWN", () => {
      expect(useGraphStore.getState().layoutDirection).toBe("DOWN");
    });

    it("should have default edge routing ORTHOGONAL", () => {
      expect(useGraphStore.getState().edgeRouting).toBe("ORTHOGONAL");
    });

    it("should have default isLayouting false", () => {
      expect(useGraphStore.getState().isLayouting).toBe(false);
    });

    it("should update layout direction", () => {
      useGraphStore.getState().setLayoutDirection("RIGHT");
      expect(useGraphStore.getState().layoutDirection).toBe("RIGHT");
    });

    it("should update edge routing", () => {
      useGraphStore.getState().setEdgeRouting("SPLINES");
      expect(useGraphStore.getState().edgeRouting).toBe("SPLINES");
    });

    it("should update isLayouting", () => {
      useGraphStore.getState().setIsLayouting(true);
      expect(useGraphStore.getState().isLayouting).toBe(true);
    });

    it("should reset layout state on reset()", () => {
      useGraphStore.getState().setLayoutDirection("LEFT");
      useGraphStore.getState().setEdgeRouting("POLYLINE");
      useGraphStore.getState().setIsLayouting(true);

      useGraphStore.getState().reset();

      expect(useGraphStore.getState().layoutDirection).toBe("DOWN");
      expect(useGraphStore.getState().edgeRouting).toBe("ORTHOGONAL");
      expect(useGraphStore.getState().isLayouting).toBe(false);
    });
  });

  describe("highlightedNodeId", () => {
    it("should default to null", () => {
      expect(useGraphStore.getState().highlightedNodeId).toBeNull();
    });

    it("should set highlighted node id", () => {
      useGraphStore.getState().setHighlightedNodeId("n1");
      expect(useGraphStore.getState().highlightedNodeId).toBe("n1");
    });

    it("should clear highlighted node id", () => {
      useGraphStore.getState().setHighlightedNodeId("n1");
      useGraphStore.getState().setHighlightedNodeId(null);
      expect(useGraphStore.getState().highlightedNodeId).toBeNull();
    });

    it("should reset highlighted node id on reset()", () => {
      useGraphStore.getState().setHighlightedNodeId("n1");
      useGraphStore.getState().reset();
      expect(useGraphStore.getState().highlightedNodeId).toBeNull();
    });
  });

  describe("selectedNodeIds", () => {
    it("should default to empty array", () => {
      expect(useGraphStore.getState().selectedNodeIds).toEqual([]);
    });

    it("should set selected node ids", () => {
      useGraphStore.getState().setSelectedNodeIds(["n1", "n2"]);
      expect(useGraphStore.getState().selectedNodeIds).toEqual(["n1", "n2"]);
    });

    it("should clear selected node ids", () => {
      useGraphStore.getState().setSelectedNodeIds(["n1"]);
      useGraphStore.getState().setSelectedNodeIds([]);
      expect(useGraphStore.getState().selectedNodeIds).toEqual([]);
    });

    it("should reset selected node ids on reset()", () => {
      useGraphStore.getState().setSelectedNodeIds(["n1", "n2"]);
      useGraphStore.getState().reset();
      expect(useGraphStore.getState().selectedNodeIds).toEqual([]);
    });
  });

  describe("proposal actions", () => {
    const createGraphData = (
      nodes: Array<{ id: string; label: string; type?: string }>,
      edges: Array<{ id: string; from: string; to: string }> = [],
    ): GraphData => ({
      meta: { version: "1", title: "Test", diagramType: "flowchart" },
      nodes: nodes.map((n) => ({
        id: n.id,
        type: n.type ?? "process",
        label: n.label,
      })),
      edges: edges.map((e) => ({ id: e.id, from: e.from, to: e.to })),
    });

    describe("proposeChanges", () => {
      it("should set proposalStatus to pending", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        expect(useGraphStore.getState().proposalStatus).toBe("pending");
      });

      it("should snapshot current nodes and edges", () => {
        useGraphStore.setState({ nodes: [makeNode("n1")], edges: [makeEdge("e1", "n1", "n2")] });
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "B" }]));

        const snapshot = useGraphStore.getState().previousGraphSnapshot;
        expect(snapshot).not.toBeNull();
        expect(snapshot!.nodes).toHaveLength(1);
        expect(snapshot!.nodes[0]!.id).toBe("n1");
        expect(snapshot!.edges).toHaveLength(1);
      });

      it("should store the proposed patch", () => {
        const graphData = createGraphData([{ id: "n1", label: "A" }]);
        useGraphStore.getState().proposeChanges(graphData);
        expect(useGraphStore.getState().proposedPatch).toEqual(graphData);
      });

      it("should clear BFS highlighting", () => {
        useGraphStore.setState({ highlightedNodeId: "n1" });
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        expect(useGraphStore.getState().highlightedNodeId).toBeNull();
      });

      it("should clear lastProposalOutcome on new proposal", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        useGraphStore.getState().acceptProposal();
        expect(useGraphStore.getState().lastProposalOutcome).toBe("accepted");
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n2", label: "B" }]));
        expect(useGraphStore.getState().lastProposalOutcome).toBeNull();
      });

      it("should add ai-diff-add className to new nodes", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "New" }]));
        const node = useGraphStore.getState().nodes.find((n) => n.id === "n1");
        expect(node?.className).toBe("ai-diff-add");
      });

      it("should add ai-diff-remove className to removed nodes", () => {
        useGraphStore.setState({ nodes: [makeNode("n1")], edges: [] });
        useGraphStore.getState().proposeChanges(createGraphData([]));
        const node = useGraphStore.getState().nodes.find((n) => n.id === "n1");
        expect(node?.className).toBe("ai-diff-remove");
      });
    });

    describe("acceptProposal", () => {
      it("should clear proposal state and apply nodes without diff classes", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "Accepted" }]));
        useGraphStore.getState().acceptProposal();

        const state = useGraphStore.getState();
        expect(state.proposalStatus).toBe("idle");
        expect(state.proposedPatch).toBeNull();
        expect(state.previousGraphSnapshot).toBeNull();
        const node = state.nodes.find((n) => n.id === "n1");
        expect(node?.className).toBeUndefined();
      });

      it("should set lastProposalOutcome to accepted", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        useGraphStore.getState().acceptProposal();
        expect(useGraphStore.getState().lastProposalOutcome).toBe("accepted");
      });

      it("should do nothing if no proposal", () => {
        useGraphStore.setState({ nodes: [makeNode("n1")], edges: [] });
        useGraphStore.getState().acceptProposal();
        expect(useGraphStore.getState().nodes[0]!.id).toBe("n1");
      });

      it("should apply layoutDirection from proposed patch", () => {
        const graphData = createGraphData([{ id: "n1", label: "A" }]);
        graphData.meta!.layoutDirection = "RIGHT";
        useGraphStore.getState().proposeChanges(graphData);
        useGraphStore.getState().acceptProposal();
        expect(useGraphStore.getState().layoutDirection).toBe("RIGHT");
      });
    });

    describe("rejectProposal", () => {
      it("should restore the previous snapshot", () => {
        useGraphStore.setState({ nodes: [makeNode("n1")], edges: [], nodeCount: 1 });
        useGraphStore.getState().proposeChanges(
          createGraphData([{ id: "n1", label: "Changed" }, { id: "n2", label: "New" }]),
        );
        useGraphStore.getState().rejectProposal();

        const state = useGraphStore.getState();
        expect(state.proposalStatus).toBe("idle");
        expect(state.proposedPatch).toBeNull();
        expect(state.previousGraphSnapshot).toBeNull();
        expect(state.nodes).toHaveLength(1);
        expect(state.nodes[0]!.id).toBe("n1");
      });

      it("should set lastProposalOutcome to rejected", () => {
        useGraphStore.setState({ nodes: [makeNode("n1")], edges: [], nodeCount: 1 });
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        useGraphStore.getState().rejectProposal();
        expect(useGraphStore.getState().lastProposalOutcome).toBe("rejected");
      });

      it("should do nothing if no snapshot", () => {
        useGraphStore.getState().rejectProposal();
        expect(useGraphStore.getState().proposalStatus).toBe("idle");
      });
    });

    describe("clearProposal", () => {
      it("should reset proposal state without affecting nodes", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        const nodesBefore = useGraphStore.getState().nodes;
        useGraphStore.getState().clearProposal();

        expect(useGraphStore.getState().proposalStatus).toBe("idle");
        expect(useGraphStore.getState().proposedPatch).toBeNull();
        expect(useGraphStore.getState().previousGraphSnapshot).toBeNull();
        // Nodes remain as-is (diff view)
        expect(useGraphStore.getState().nodes).toEqual(nodesBefore);
      });
    });

    describe("reset with proposal", () => {
      it("should reset proposal state along with everything else", () => {
        useGraphStore.getState().proposeChanges(createGraphData([{ id: "n1", label: "A" }]));
        useGraphStore.getState().reset();

        const state = useGraphStore.getState();
        expect(state.proposalStatus).toBe("idle");
        expect(state.proposedPatch).toBeNull();
        expect(state.previousGraphSnapshot).toBeNull();
        expect(state.nodes).toHaveLength(0);
      });
    });
  });

  describe("prefillChat", () => {
    it("should default to null", () => {
      expect(useGraphStore.getState().prefillChat).toBeNull();
    });

    it("should set prefillChat with nodeId and text", () => {
      useGraphStore.getState().setPrefillChat("n1", "Explain this element");
      const prefill = useGraphStore.getState().prefillChat;
      expect(prefill).toEqual({ nodeId: "n1", text: "Explain this element" });
    });

    it("should clear prefillChat", () => {
      useGraphStore.getState().setPrefillChat("n1", "text");
      useGraphStore.getState().clearPrefillChat();
      expect(useGraphStore.getState().prefillChat).toBeNull();
    });

    it("should overwrite previous prefillChat", () => {
      useGraphStore.getState().setPrefillChat("n1", "first");
      useGraphStore.getState().setPrefillChat("n2", "second");
      expect(useGraphStore.getState().prefillChat).toEqual({ nodeId: "n2", text: "second" });
    });

    it("should reset prefillChat on reset()", () => {
      useGraphStore.getState().setPrefillChat("n1", "text");
      useGraphStore.getState().reset();
      expect(useGraphStore.getState().prefillChat).toBeNull();
    });
  });

  describe("fitViewRequested", () => {
    it("should default to 0", () => {
      expect(useGraphStore.getState().fitViewRequested).toBe(0);
    });

    it("should increment on requestFitView", () => {
      useGraphStore.getState().requestFitView();
      expect(useGraphStore.getState().fitViewRequested).toBe(1);
    });

    it("should increment each call", () => {
      useGraphStore.getState().requestFitView();
      useGraphStore.getState().requestFitView();
      useGraphStore.getState().requestFitView();
      expect(useGraphStore.getState().fitViewRequested).toBe(3);
    });

    it("should reset fitViewRequested on reset()", () => {
      useGraphStore.getState().requestFitView();
      useGraphStore.getState().reset();
      expect(useGraphStore.getState().fitViewRequested).toBe(0);
    });
  });

  describe("focusNodeId", () => {
    it("should default to null", () => {
      expect(useGraphStore.getState().focusNodeId).toBeNull();
    });

    it("should set focusNodeId", () => {
      useGraphStore.getState().setFocusNodeId("n1");
      expect(useGraphStore.getState().focusNodeId).toBe("n1");
    });

    it("should clear focusNodeId", () => {
      useGraphStore.getState().setFocusNodeId("n1");
      useGraphStore.getState().setFocusNodeId(null);
      expect(useGraphStore.getState().focusNodeId).toBeNull();
    });

    it("should reset focusNodeId on reset()", () => {
      useGraphStore.getState().setFocusNodeId("n1");
      useGraphStore.getState().reset();
      expect(useGraphStore.getState().focusNodeId).toBeNull();
    });
  });
});
