import { describe, it, expect, beforeEach } from "vitest";
import {
  useDiagramStore,
  snapToGridValue,
  snapPosition,
  normaliseRotation,
  GRID_SIZE,
} from "./diagramStore";

function reset() {
  useDiagramStore.setState({
    nodes: [],
    edges: [],
    selectedNodeId: null,
    fluid: "R410A",
    fluidBackend: "CoolProp",
    controls: [],
    snapToGrid: true,
    result: null,
    simulating: false,
    simError: null,
  });
}

beforeEach(reset);

describe("controls import", () => {
  it("does not materialise Fixed/Free calib_* as canvas SaturatedController nodes", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "BphxEvaporator", name: "evap", z_ua: 1 },
            { type: "Probe", name: "SST probe", tsat_c: 4.4 },
          ],
          edges: [{ from: "evap:outlet", to: "SST probe:inlet" }],
        },
      ],
      controls: [
        {
          type: "SaturatedController",
          id: "calib_evap_z_ua",
          measure: { component: "SST probe", output: "saturationTemperature" },
          actuator: { component: "evap", factor: "z_ua", initial: 1, min: 0.1, max: 3 },
          target: 277.55,
        },
        {
          type: "SaturatedController",
          id: "dgt_limiter",
          measure: { component: "evap", output: "temperature" },
          actuator: { component: "evap", factor: "injection", initial: 0.1, min: 0, max: 0.3 },
          target: 300,
        },
      ],
    });

    const st = useDiagramStore.getState();
    expect(st.nodes.map((n) => n.data.name)).not.toContain("calib_evap_z_ua");
    expect(st.nodes.map((n) => n.data.name)).toContain("dgt_limiter");
    expect(st.controls.map((c) => c.id)).toEqual(["dgt_limiter"]);
  });

  it("purgeAutoCalibrationControlNodes removes leftover calib_* canvas blocks", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [{ type: "BphxEvaporator", name: "evap" }],
          edges: [],
        },
      ],
      controls: [
        {
          id: "calib_evap_z_ua",
          measure: { component: "evap", output: "saturationTemperature" },
          actuator: { component: "evap", factor: "z_ua", min: 0.1, max: 3 },
          target: 277.55,
        },
      ],
    });
    // Simulate a stale canvas block from an older import path.
    useDiagramStore.setState((s) => ({
      nodes: [
        ...s.nodes,
        {
          id: "stale-calib",
          type: "entropykNode",
          position: { x: 0, y: 0 },
          data: {
            type: "SaturatedController",
            name: "calib_evap_z_ua",
            circuit: 0,
            rotation: 0,
            flipH: false,
            flipV: false,
            params: {},
          },
        },
      ],
    }));
    useDiagramStore.getState().purgeAutoCalibrationControlNodes();
    expect(useDiagramStore.getState().nodes.map((n) => n.data.name)).not.toContain(
      "calib_evap_z_ua",
    );
  });

  it("preserves imported co-solved controls for the next simulation run", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [{ type: "IsentropicCompressor", name: "comp", liquid_injection: true }],
          edges: [],
        },
      ],
      controls: [
        {
          type: "SaturatedController",
          id: "dgt_limiter",
          measure: { component: "comp", output: "temperature" },
          actuator: { component: "comp", factor: "injection", initial: 0.15, min: 0, max: 0.3 },
          target: 330,
          gain: -0.5,
          band: 5,
          alpha: 0.002,
          objectives: [
            {
              component: "comp",
              output: "temperature",
              setpoint: 365,
              gain: -0.05,
              combine: "min",
            },
          ],
        },
      ],
      solver: { strategy: "newton", max_iterations: 300, tolerance: 1e-6 },
    });

    expect(useDiagramStore.getState().controls).toEqual([
      expect.objectContaining({
        id: "dgt_limiter",
        actuator: expect.objectContaining({ factor: "injection", initial: 0.15 }),
        target: 330,
      }),
    ]);
    expect(useDiagramStore.getState().nodes).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          data: expect.objectContaining({
            type: "SaturatedController",
            name: "dgt_limiter",
            params: expect.objectContaining({
              measure_component: "comp",
              actuator_factor: "injection",
              alpha: 0.002,
              objectives_json: JSON.stringify([
                {
                  component: "comp",
                  output: "temperature",
                  setpoint: 365,
                  gain: -0.05,
                  combine: "min",
                },
              ]),
            }),
          }),
        }),
      ]),
    );
  });
});

describe("grid helpers", () => {
  it("snaps a value to the nearest grid line", () => {
    expect(snapToGridValue(5, 12)).toBe(0);
    expect(snapToGridValue(7, 12)).toBe(12);
    expect(snapToGridValue(18, 12)).toBe(24);
  });

  it("snaps an {x, y} position", () => {
    expect(snapPosition({ x: 7, y: 5 }, 12)).toEqual({ x: 12, y: 0 });
  });

  it("normalises rotation to 0/90/180/270", () => {
    expect(normaliseRotation(0)).toBe(0);
    expect(normaliseRotation(90)).toBe(90);
    expect(normaliseRotation(360)).toBe(0);
    expect(normaliseRotation(-90)).toBe(270);
    expect(normaliseRotation(450)).toBe(90);
  });
});

describe("addComponent", () => {
  it("creates a node with defaults, rotation 0, and selects it", () => {
    useDiagramStore.getState().addComponent("Condenser", { x: 100, y: 100 });
    const { nodes, selectedNodeId } = useDiagramStore.getState();
    expect(nodes).toHaveLength(1);
    expect(nodes[0].data.type).toBe("Condenser");
    expect(nodes[0].data.rotation).toBe(0);
    expect(nodes[0].data.params.ua).toBe(5000);
    expect(selectedNodeId).toBe(nodes[0].id);
  });

  it("snaps the drop position when snap is enabled", () => {
    useDiagramStore.getState().addComponent("Condenser", { x: 103, y: 97 });
    const n = useDiagramStore.getState().nodes[0];
    expect(n.position.x % GRID_SIZE).toBe(0);
    expect(n.position.y % GRID_SIZE).toBe(0);
  });

  it("keeps the raw position when snap is disabled", () => {
    useDiagramStore.setState({ snapToGrid: false });
    useDiagramStore.getState().addComponent("Condenser", { x: 103, y: 97 });
    const n = useDiagramStore.getState().nodes[0];
    expect(n.position).toEqual({ x: 103, y: 97 });
  });

  it("generates unique names per type", () => {
    const s = useDiagramStore.getState();
    s.addComponent("Condenser", { x: 0, y: 0 });
    s.addComponent("Condenser", { x: 0, y: 0 });
    const names = useDiagramStore.getState().nodes.map((n) => n.data.name);
    expect(new Set(names).size).toBe(2);
  });
});

describe("node mutations", () => {
  it("updates params, name, and circuit", () => {
    useDiagramStore.getState().addComponent("Condenser", { x: 0, y: 0 });
    const id = useDiagramStore.getState().nodes[0].id;
    const s = useDiagramStore.getState();
    s.updateNodeParams(id, { ua: 9999 });
    s.updateNodeName(id, "my_cond");
    s.updateNodeCircuit(id, 2);
    const n = useDiagramStore.getState().nodes[0];
    expect(n.data.params.ua).toBe(9999);
    expect(n.data.name).toBe("my_cond");
    expect(n.data.circuit).toBe(2);
  });

  it("rotates clockwise and counter-clockwise with wrap-around", () => {
    useDiagramStore.getState().addComponent("Compressor", { x: 0, y: 0 });
    const id = useDiagramStore.getState().nodes[0].id;
    const { rotateNode } = useDiagramStore.getState();
    rotateNode(id, 1);
    expect(useDiagramStore.getState().nodes[0].data.rotation).toBe(90);
    rotateNode(id, 1);
    rotateNode(id, 1);
    rotateNode(id, 1);
    expect(useDiagramStore.getState().nodes[0].data.rotation).toBe(0);
    rotateNode(id, -1);
    expect(useDiagramStore.getState().nodes[0].data.rotation).toBe(270);
  });

  it("flips horizontal and vertical like Modelica", () => {
    useDiagramStore.getState().addComponent("RefrigerantPipe", { x: 0, y: 0 });
    const id = useDiagramStore.getState().nodes[0].id;
    expect(useDiagramStore.getState().nodes[0].data.flipH).toBe(false);
    useDiagramStore.getState().flipNodeH(id);
    expect(useDiagramStore.getState().nodes[0].data.flipH).toBe(true);
    useDiagramStore.getState().flipNodeV(id);
    expect(useDiagramStore.getState().nodes[0].data.flipV).toBe(true);
  });
});

describe("connections and removal", () => {
  it("adds an edge on connect", () => {
    const s = useDiagramStore.getState();
    s.addComponent("IsentropicCompressor", { x: 0, y: 0 });
    s.addComponent("Condenser", { x: 200, y: 0 });
    const [a, b] = useDiagramStore.getState().nodes;
    useDiagramStore.getState().onConnect({
      source: a.id,
      target: b.id,
      sourceHandle: "outlet",
      targetHandle: "inlet",
    });
    const edges = useDiagramStore.getState().edges;
    expect(edges).toHaveLength(1);
    expect(edges[0].source).toBe(a.id);
    expect(edges[0].target).toBe(b.id);
  });

  it("removes a node along with its connected edges and clears selection", () => {
    const s = useDiagramStore.getState();
    s.addComponent("IsentropicCompressor", { x: 0, y: 0 });
    s.addComponent("Condenser", { x: 200, y: 0 });
    const [a, b] = useDiagramStore.getState().nodes;
    useDiagramStore.getState().onConnect({
      source: a.id,
      target: b.id,
      sourceHandle: "outlet",
      targetHandle: "inlet",
    });
    useDiagramStore.getState().setSelected(a.id);
    useDiagramStore.getState().removeNode(a.id);
    const st = useDiagramStore.getState();
    expect(st.nodes).toHaveLength(1);
    expect(st.edges).toHaveLength(0);
    expect(st.selectedNodeId).toBeNull();
  });

  it("clears selection when the selected node is removed via onNodesChange", () => {
    useDiagramStore.getState().addComponent("Condenser", { x: 0, y: 0 });
    const id = useDiagramStore.getState().nodes[0].id;
    useDiagramStore.getState().setSelected(id);
    useDiagramStore.getState().onNodesChange([{ type: "remove", id }]);
    expect(useDiagramStore.getState().selectedNodeId).toBeNull();
    expect(useDiagramStore.getState().nodes).toHaveLength(0);
  });
});

describe("toggleSnapToGrid", () => {
  it("flips the snap flag", () => {
    expect(useDiagramStore.getState().snapToGrid).toBe(true);
    useDiagramStore.getState().toggleSnapToGrid();
    expect(useDiagramStore.getState().snapToGrid).toBe(false);
    useDiagramStore.getState().toggleSnapToGrid();
    expect(useDiagramStore.getState().snapToGrid).toBe(true);
  });
});

describe("loadFromConfig and clear", () => {
  const config = {
    fluid: "R290",
    fluid_backend: "Test",
    circuits: [
      {
        id: 0,
        components: [
          { type: "IsentropicCompressor", name: "comp", isentropic_efficiency: 0.7 },
          { type: "Condenser", name: "cond", ua: 5000 },
        ],
        edges: [{ from: "comp:outlet", to: "cond:inlet" }],
      },
    ],
  };

  it("rebuilds nodes, edges, rotation and fluid from a config", () => {
    useDiagramStore.getState().loadFromConfig(config);
    const st = useDiagramStore.getState();
    expect(st.nodes).toHaveLength(2);
    expect(st.edges).toHaveLength(1);
    expect(st.fluid).toBe("R290");
    expect(st.fluidBackend).toBe("Test");
    for (const n of st.nodes) expect(n.data.rotation).toBe(0);
    // the params should be carried over as primitives
    const comp = st.nodes.find((n) => n.data.name === "comp");
    expect(comp?.data.params.isentropic_efficiency).toBe(0.7);
  });

  it("ignores configs without circuits", () => {
    useDiagramStore.getState().addComponent("Condenser", { x: 0, y: 0 });
    useDiagramStore.getState().loadFromConfig({ foo: "bar" });
    expect(useDiagramStore.getState().nodes).toHaveLength(1);
  });

  it("flattens subsystem instances from system model JSON", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      fluid_backend: "CoolProp",
      subsystems: {
        Circuit: {
          params: { ua_cond: 700 },
          components: [
            { type: "IsentropicCompressor", name: "comp", isentropic_efficiency: 0.7 },
            { type: "Condenser", name: "cond", ua: "$ua_cond" },
          ],
          edges: [{ from: "comp:outlet", to: "cond:inlet" }],
          ports: { discharge: "comp:outlet" },
        },
      },
      instances: [
        { of: "Circuit", name: "A", circuit: 0, params: { ua_cond: 800 } },
        { of: "Circuit", name: "B", circuit: 1 },
      ],
    });

    const st = useDiagramStore.getState();
    expect(st.nodes).toHaveLength(4);
    expect(st.edges).toHaveLength(2);
    expect(st.nodes.map((n) => n.data.name)).toContain("A.comp");
    expect(st.nodes.map((n) => n.data.name)).toContain("B.cond");
    expect(st.nodes.find((n) => n.data.name === "A.cond")?.data.params.ua).toBe(800);
    expect(st.nodes.find((n) => n.data.name === "B.cond")?.data.params.ua).toBe(700);
  });

  it("keeps explicit water circuits while flattening refrigerant subsystem instances", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 2,
          components: [
            { type: "BrineSource", name: "water_in", t_set_c: 30 },
            { type: "ThermalLoad", name: "water_load", mass_flow_kg_s: 1.0 },
            { type: "BrineSink", name: "water_out" },
          ],
          edges: [
            { from: "water_in:outlet", to: "water_load:inlet" },
            { from: "water_load:outlet", to: "water_out:inlet" },
          ],
        },
      ],
      subsystems: {
        Circuit: {
          components: [
            { type: "IsentropicCompressor", name: "comp", isentropic_efficiency: 0.7 },
            { type: "Condenser", name: "cond", ua: 700 },
          ],
          edges: [{ from: "comp:outlet", to: "cond:inlet" }],
        },
      },
      instances: [{ of: "Circuit", name: "A", circuit: 0 }],
    });

    const st = useDiagramStore.getState();
    expect(st.nodes.map((n) => n.data.name)).toEqual(
      expect.arrayContaining(["A.comp", "A.cond", "water_in", "water_load", "water_out"]),
    );
    expect(st.edges).toHaveLength(3);
    expect(st.nodes.find((n) => n.data.name === "water_load")?.data.circuit).toBe(2);
  });

  it("lays out imported chiller cycles as a horizontal HVAC schematic", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "IsentropicCompressor", name: "comp", liquid_injection: true },
            { type: "Condenser", name: "cond", ua: 5000 },
            { type: "IsenthalpicExpansionValve", name: "exv", opening: 0.8 },
            { type: "Evaporator", name: "evap", ua: 6000 },
            { type: "BrineSource", name: "cond_water_in", t_set_c: 30 },
            { type: "BrineSink", name: "cond_water_out" },
            { type: "BrineSource", name: "evap_water_in", t_set_c: 12 },
            { type: "BrineSink", name: "evap_water_out" },
          ],
          edges: [
            { from: "comp:outlet", to: "cond:inlet" },
            { from: "cond:outlet", to: "exv:inlet" },
            { from: "exv:outlet", to: "evap:inlet" },
            { from: "evap:outlet", to: "comp:inlet" },
            { from: "cond_water_in:outlet", to: "cond:secondary_inlet" },
            { from: "cond:secondary_outlet", to: "cond_water_out:inlet" },
            { from: "evap_water_in:outlet", to: "evap:secondary_inlet" },
            { from: "evap:secondary_outlet", to: "evap_water_out:inlet" },
          ],
        },
      ],
      controls: [
        {
          id: "dgt_limiter",
          measure: { component: "comp", output: "temperature" },
          actuator: { component: "comp", factor: "injection", initial: 0.15, min: 0, max: 0.3 },
          target: 330,
        },
      ],
    });

    const nodes = useDiagramStore.getState().nodes;
    const byName = Object.fromEntries(nodes.map((node) => [node.data.name, node]));

    expect(byName.evap.position.x).toBeLessThan(byName.comp.position.x);
    expect(byName.comp.position.x).toBeLessThan(byName.cond.position.x);
    expect(byName.exv.position.x).toBeGreaterThan(byName.cond.position.x);
    expect(byName.exv.position.y).toBeGreaterThan(byName.cond.position.y);
    expect(byName.dgt_limiter.position.y).toBeLessThan(byName.comp.position.y);

    expect(byName.evap_water_out.position.y).toBeLessThan(byName.evap.position.y);
    expect(byName.evap_water_in.position.y).toBeGreaterThan(byName.evap.position.y);
    expect(byName.cond_water_out.position.y).toBeLessThan(byName.cond.position.y);
    expect(byName.cond_water_in.position.y).toBeGreaterThan(byName.cond.position.y);
    expect(useDiagramStore.getState().edges).toHaveLength(8);
  });

  it("uses deterministic fallback positions for partial imported configs", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "Pipe", name: "pipe_b", length_m: 2 },
            { type: "Placeholder", name: "custom_a", n_equations: 1 },
          ],
          edges: [{ from: "custom_a:outlet", to: "pipe_b:inlet" }],
        },
      ],
    });

    const first = useDiagramStore.getState().nodes.map((node) => [node.data.name, node.position]);
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "Pipe", name: "pipe_b", length_m: 2 },
            { type: "Placeholder", name: "custom_a", n_equations: 1 },
          ],
          edges: [{ from: "custom_a:outlet", to: "pipe_b:inlet" }],
        },
      ],
    });

    expect(useDiagramStore.getState().nodes.map((node) => [node.data.name, node.position])).toEqual(first);
    expect(useDiagramStore.getState().edges).toHaveLength(1);
  });

  it("places secondary boundaries near generic four-port heat exchangers", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "HeatExchanger", name: "hx", ua: 3000 },
            { type: "BrineSource", name: "water_in", t_set_c: 20 },
            { type: "BrineSink", name: "water_out" },
            { type: "AirSource", name: "air_in", t_dry_c: 15 },
            { type: "AirSink", name: "air_out" },
          ],
          edges: [
            { from: "water_in:outlet", to: "hx:hot_inlet" },
            { from: "hx:hot_outlet", to: "water_out:inlet" },
            { from: "air_in:outlet", to: "hx:cold_inlet" },
            { from: "hx:cold_outlet", to: "air_out:inlet" },
          ],
        },
      ],
    });

    const byName = Object.fromEntries(useDiagramStore.getState().nodes.map((node) => [node.data.name, node]));
    expect(Math.abs(byName.water_in.position.x - byName.hx.position.x)).toBeLessThan(220);
    expect(Math.abs(byName.air_in.position.x - byName.hx.position.x)).toBeLessThan(220);
    expect(byName.water_out.position.y).toBeLessThan(byName.hx.position.y);
    expect(byName.air_out.position.y).toBeLessThan(byName.hx.position.y);
    expect(byName.water_in.position.y).toBeGreaterThan(byName.hx.position.y);
    expect(byName.air_in.position.y).toBeGreaterThan(byName.hx.position.y);
  });

  it("separates multiple imported refrigeration circuits horizontally", () => {
    useDiagramStore.getState().loadFromConfig({
      fluid: "R134a",
      circuits: [
        {
          id: 0,
          components: [
            { type: "IsentropicCompressor", name: "comp_a" },
            { type: "Condenser", name: "cond_a" },
          ],
          edges: [{ from: "comp_a:outlet", to: "cond_a:inlet" }],
        },
        {
          id: 1,
          components: [
            { type: "IsentropicCompressor", name: "comp_b" },
            { type: "Condenser", name: "cond_b" },
          ],
          edges: [{ from: "comp_b:outlet", to: "cond_b:inlet" }],
        },
      ],
    });

    const byName = Object.fromEntries(useDiagramStore.getState().nodes.map((node) => [node.data.name, node]));
    expect(byName.comp_b.position.x - byName.comp_a.position.x).toBeGreaterThan(900);
    expect(byName.cond_b.position.x - byName.cond_a.position.x).toBeGreaterThan(900);
  });

  it("clear() empties the sheet", () => {
    useDiagramStore.getState().loadFromConfig(config);
    useDiagramStore.getState().clear();
    const st = useDiagramStore.getState();
    expect(st.nodes).toHaveLength(0);
    expect(st.edges).toHaveLength(0);
    expect(st.selectedNodeId).toBeNull();
  });
});
