import { describe, expect, it } from "vitest";
import type { Edge, Node } from "@xyflow/react";
import {
  classifyParamDof,
  computeDofLedger,
  estimateComponentEquations,
} from "./dofLedger";
import type { EntropykNodeData } from "./configBuilder";

function node(
  id: string,
  type: string,
  name: string,
  params: Record<string, number | string | boolean> = {},
): Node<EntropykNodeData> {
  return {
    id,
    type: "entropyk",
    position: { x: 0, y: 0 },
    data: { type, name, circuit: 0, params },
  };
}

function edge(
  id: string,
  source: string,
  target: string,
  sourceHandle: string,
  targetHandle: string,
): Edge {
  return { id, source, target, sourceHandle, targetHandle };
}

describe("computeDofLedger", () => {
  it("returns empty for no nodes", () => {
    const L = computeDofLedger([], []);
    expect(L.balance).toBe("empty");
    expect(L.nEquations).toBe(0);
    expect(L.nUnknowns).toBe(0);
  });

  it("matches server DoF for HX air–water + fan (12=12)", () => {
    // Topology of hx_air_water_4port.json — previously the client said 10<14
    // because HeatExchanger was counted as 2 eqs and cold_* collapsed into ref.
    const nodes = [
      node("hwi", "BrineSource", "hot_water_in", {
        m_flow_kg_s: 0.5,
        t_set_c: 60,
        fix_pressure: false,
        fix_temperature: true,
        fix_mass_flow: true,
      }),
      node("hx", "HeatExchanger", "hx", { ua: 3000 }),
      node("hwo", "BrineSink", "hot_water_out", { fix_pressure: true }),
      node("cai", "AirSource", "cold_air_in", {
        m_flow_kg_s: 1.0,
        t_dry_c: 20,
        fix_pressure: false,
        fix_temperature: true,
        fix_mass_flow: true,
      }),
      node("fan", "Fan", "supply_fan"),
      node("cao", "AirSink", "cold_air_out", { fix_pressure: true }),
    ];
    const edges = [
      edge("1", "hwi", "hx", "outlet", "hot_inlet"),
      edge("2", "hx", "hwo", "hot_outlet", "inlet"),
      edge("3", "cai", "fan", "outlet", "inlet"),
      edge("4", "fan", "hx", "outlet", "cold_inlet"),
      edge("5", "hx", "cao", "cold_outlet", "inlet"),
    ];
    const L = computeDofLedger(nodes, edges);
    expect(L.nEdges).toBe(5);
    expect(L.nBranches).toBe(2); // water + air
    expect(L.nEquations).toBe(12);
    expect(L.nUnknowns).toBe(12);
    expect(L.balance).toBe("balanced");
  });

  it("counts a water-cooled 4-port chiller close to 19=19", () => {
    // Rough topology of chiller_watercooled_r410a.json
    const nodes = [
      node("c", "IsentropicCompressor", "comp", { emergent_pressure: true }),
      node("cd", "Condenser", "cond", { emergent_pressure: true }),
      node("x", "IsenthalpicExpansionValve", "exv", { emergent_pressure: true }),
      node("e", "Evaporator", "evap", { emergent_pressure: true }),
      node("cwi", "BrineSource", "cond_water_in", { m_flow_kg_s: 0.4 }),
      node("cwo", "BrineSink", "cond_water_out"),
      node("ewi", "BrineSource", "evap_water_in", { m_flow_kg_s: 0.5 }),
      node("ewo", "BrineSink", "evap_water_out"),
    ];
    const edges = [
      edge("1", "c", "cd", "outlet", "inlet"),
      edge("2", "cd", "x", "outlet", "inlet"),
      edge("3", "x", "e", "outlet", "inlet"),
      edge("4", "e", "c", "outlet", "inlet"),
      edge("5", "cwi", "cd", "outlet", "secondary_inlet"),
      edge("6", "cd", "cwo", "secondary_outlet", "inlet"),
      edge("7", "ewi", "e", "outlet", "secondary_inlet"),
      edge("8", "e", "ewo", "secondary_outlet", "inlet"),
    ];
    const L = computeDofLedger(nodes, edges);
    // Expect square-ish: honest budget is 19. Client estimate should match.
    expect(L.nEdges).toBe(8);
    expect(L.nBranches).toBe(3); // ref + cw + chw
    expect(L.nUnknowns).toBe(3 + 2 * 8); // 19
    expect(L.nEquations).toBe(19);
    expect(L.balance).toBe("balanced");
  });

  it("flags quality_control without free actuator as over-constrained risk", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", { quality_control: true }),
      node("s", "BrineSource", "src", { m_flow_kg_s: 1 }),
      node("k", "BrineSink", "sink"),
    ];
    const edges = [
      edge("a", "s", "e", "outlet", "secondary_inlet"),
      edge("b", "e", "k", "secondary_outlet", "inlet"),
    ];
    const est = estimateComponentEquations(nodes[0], edges, nodes);
    expect(est.nEquations).toBe(5); // ΔP + energy + outlet_closure + P_sec + energy_sec
    expect(est.roles.some((r) => r.includes("quality"))).toBe(true);
  });

  it("ignores orphan pipes in the equation count (no hard-block)", () => {
    const nodes = [
      node("c", "IsentropicCompressor", "comp", { emergent_pressure: true }),
      node("cd", "Condenser", "cond", { emergent_pressure: true }),
      node("x", "IsenthalpicExpansionValve", "exv", { emergent_pressure: true }),
      node("e", "Evaporator", "evap", { emergent_pressure: true }),
      node("cwi", "BrineSource", "cond_water_in", { m_flow_kg_s: 0.4 }),
      node("cwo", "BrineSink", "cond_water_out"),
      node("ewi", "BrineSource", "evap_water_in", { m_flow_kg_s: 0.5 }),
      node("ewo", "BrineSink", "evap_water_out"),
      node("p", "RefrigerantPipe", "line1", { length_m: 3, diameter_m: 0.012 }),
    ];
    const edges = [
      edge("1", "c", "cd", "outlet", "inlet"),
      edge("2", "cd", "x", "outlet", "inlet"),
      edge("3", "x", "e", "outlet", "inlet"),
      edge("4", "e", "c", "outlet", "inlet"),
      edge("5", "cwi", "cd", "outlet", "secondary_inlet"),
      edge("6", "cd", "cwo", "secondary_outlet", "inlet"),
      edge("7", "ewi", "e", "outlet", "secondary_inlet"),
      edge("8", "e", "ewo", "secondary_outlet", "inlet"),
    ];
    const L = computeDofLedger(nodes, edges);
    expect(L.balance).toBe("balanced");
    expect(L.components.find((c) => c.name === "line1")?.nEquations).toBe(0);
  });

  it("counts a spliced refrigerant pipe as +2 eqs / +1 edge (neutral DoF)", () => {
    const nodes = [
      node("c", "IsentropicCompressor", "comp", { emergent_pressure: true }),
      node("cd", "Condenser", "cond", { emergent_pressure: true }),
      node("x", "IsenthalpicExpansionValve", "exv", { emergent_pressure: true }),
      node("e", "Evaporator", "evap", { emergent_pressure: true }),
      node("cwi", "BrineSource", "cond_water_in", { m_flow_kg_s: 0.4 }),
      node("cwo", "BrineSink", "cond_water_out"),
      node("ewi", "BrineSource", "evap_water_in", { m_flow_kg_s: 0.5 }),
      node("ewo", "BrineSink", "evap_water_out"),
      node("p", "RefrigerantPipe", "line1", { length_m: 3, diameter_m: 0.012 }),
    ];
    const edges = [
      edge("1a", "c", "p", "outlet", "inlet"),
      edge("1b", "p", "cd", "outlet", "inlet"),
      edge("2", "cd", "x", "outlet", "inlet"),
      edge("3", "x", "e", "outlet", "inlet"),
      edge("4", "e", "c", "outlet", "inlet"),
      edge("5", "cwi", "cd", "outlet", "secondary_inlet"),
      edge("6", "cd", "cwo", "secondary_outlet", "inlet"),
      edge("7", "ewi", "e", "outlet", "secondary_inlet"),
      edge("8", "e", "ewo", "secondary_outlet", "inlet"),
    ];
    const L = computeDofLedger(nodes, edges);
    expect(L.nEdges).toBe(9);
    expect(L.nEquations).toBe(21);
    expect(L.nUnknowns).toBe(L.nBranches + 2 * 9);
    expect(L.balance).toBe("balanced");
  });
});

describe("classifyParamDof", () => {
  it("marks emergent t_sat as seed not fix", () => {
    const tag = classifyParamDof("Condenser", "t_sat_k", { emergent_pressure: true });
    expect(tag?.kind).toBe("solved");
  });

  it("marks boundary T as FIX", () => {
    const tag = classifyParamDof("BrineSource", "t_set_c", { t_set_c: 12 });
    expect(tag?.kind).toBe("fixed");
  });

  it("marks Free ṁ on BrineSource as FREE", () => {
    const tag = classifyParamDof("BrineSource", "m_flow_kg_s", {
      m_flow_kg_s: 0.5,
      __fixed_m_flow_kg_s: false,
    });
    expect(tag?.kind).toBe("free");
  });

  it("flags Fixed T_out + Fixed ṁ as ΔT rating conflict", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", { ua: 8000 }),
      node("s", "BrineSource", "src", {
        p_set_bar: 3,
        t_set_c: 12,
        m_flow_kg_s: 0.55,
        __fixed_m_flow_kg_s: true,
      }),
      node("k", "BrineSink", "sink", {
        p_back_bar: 3,
        t_set_c: 7,
        __fixed_t_set_c: true,
      }),
    ];
    const edges = [
      edge("1", "s", "e", "outlet", "secondary_inlet"),
      edge("2", "e", "k", "secondary_outlet", "inlet"),
    ];
    const ledger = computeDofLedger(nodes, edges);
    expect(ledger.diagnostics.some((d) => d.includes("Fixed T_out") && d.includes("Fixed ṁ"))).toBe(
      true,
    );
  });

  it("marks scalar secondary as risk without live ports", () => {
    const tag = classifyParamDof(
      "FloodedEvaporator",
      "secondary_inlet_temp_c",
      { secondary_inlet_temp_c: 12 },
      { hasLiveSecondary: false },
    );
    expect(tag?.kind).toBe("risk");
  });
});
