import { describe, it, expect } from "vitest";
import type { Edge, Node } from "@xyflow/react";
import {
  applyBoundaryFixSemantics,
  buildScenarioConfig,
  buildModelEmbeddings,
  buildFixedFreeCalibrationControls,
  resolveSecondaryStreams,
  stripUiOnlyParams,
  validateConfig,
  SCHEMA_VERSION,
  type EntropykNodeData,
} from "./configBuilder";
import { fixedFlagKey } from "./componentMeta";

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

describe("buildScenarioConfig", () => {
  it("groups components by circuit, sorted by id", () => {
    const nodes = [
      node("a", "Condenser", "cond", 1, { ua: 5000 }),
      node("b", "Evaporator", "evap", 0, { ua: 6000 }),
    ];
    const cfg = buildScenarioConfig(nodes, []);
    expect(cfg.circuits.map((c) => c.id)).toEqual([0, 1]);
    expect(cfg.circuits[0].components[0].name).toBe("evap");
    expect(cfg.circuits[1].components[0].name).toBe("cond");
  });

  it("flattens params to the top level of each component", () => {
    const nodes = [node("a", "Condenser", "cond", 0, { ua: 5000, t_sat_k: 320 })];
    const cfg = buildScenarioConfig(nodes, []);
    const comp = cfg.circuits[0].components[0];
    expect(comp).toMatchObject({ type: "Condenser", name: "cond", ua: 5000, t_sat_k: 320 });
  });

  it("translates edge handles into name:port references", () => {
    const nodes = [
      node("a", "IsentropicCompressor", "comp", 0),
      node("b", "Condenser", "cond", 0),
    ];
    const edges: Edge[] = [
      { id: "e1", source: "a", target: "b", sourceHandle: "outlet", targetHandle: "inlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.circuits[0].edges).toEqual([{ from: "comp:outlet", to: "cond:inlet" }]);
  });

  it("only keeps edges whose endpoints are in the same circuit", () => {
    const nodes = [
      node("a", "IsentropicCompressor", "comp", 0),
      node("b", "Condenser", "cond", 1),
    ];
    const edges: Edge[] = [
      { id: "e1", source: "a", target: "b", sourceHandle: "outlet", targetHandle: "inlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.circuits[0].edges).toHaveLength(0);
    expect(cfg.circuits[1].edges).toHaveLength(0);
  });

  it("falls back to outlet→inlet when handles are missing", () => {
    const nodes = [
      node("a", "IsentropicCompressor", "comp", 0),
      node("b", "Condenser", "cond", 0),
    ];
    const edges: Edge[] = [
      { id: "e1", source: "a", target: "b", sourceHandle: null, targetHandle: null },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    // Source role → outlet-like; target role → inlet-like
    expect(cfg.circuits[0].edges[0]).toEqual({ from: "comp:outlet", to: "cond:inlet" });
  });

  it("applies fluid / backend / solver options and defaults", () => {
    const cfg = buildScenarioConfig([node("a", "Condenser", "cond", 0)], [], {
      fluid: "R290",
      fluidBackend: "Test",
      maxIterations: 50,
    });
    expect(cfg.fluid).toBe("R290");
    expect(cfg.fluid_backend).toBe("Test");
    expect(cfg.solver.max_iterations).toBe(50);
    expect(cfg.solver.tolerance).toBe(1e-6);

    const defaults = buildScenarioConfig([], []);
    expect(defaults.fluid).toBe("R410A");
    expect(defaults.fluid_backend).toBe("CoolProp");
    expect(defaults.solver.strategy).toBe("newton");
  });

  it("emits instances and connections for ModuleInstance nodes in Schema v2", () => {
    const nodes = [
      node("m1", "ModuleInstance", "c1", 0, { module_name: "BaseChiller" }),
      node("m2", "ModuleInstance", "c2", 0, { module_name: "BaseChiller", ua_cond: 800 }),
    ];
    const edges: Edge[] = [
      { id: "e1", source: "m1", target: "m2", sourceHandle: "chw_out", targetHandle: "chw_in" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.schema_version).toBe("2");
    expect(cfg.instances).toEqual([
      { of: "BaseChiller", name: "c1", circuit: 0 },
      { of: "BaseChiller", name: "c2", circuit: 0, params: { ua_cond: 800 } },
    ]);
    expect(cfg.connections).toEqual([{ from: "c1.chw_out", to: "c2.chw_in" }]);
  });

  it("emits the unified IR schema_version", () => {
    const cfg = buildScenarioConfig([node("a", "Condenser", "cond", 0)], []);
    expect(cfg.schema_version).toBe(SCHEMA_VERSION);
    expect(cfg.schema_version).toBe("2");
  });

  it("passes control loops through into the IR", () => {
    const controls = [
      {
        type: "SaturatedController",
        id: "cap",
        measure: { component: "cond", output: "capacity" },
        actuator: { component: "comp", factor: "f_m", initial: 1.0, min: 0.5, max: 1.5 },
        target: 7000,
        gain: 0.01,
        band: 1.0,
      },
    ];
    const cfg = buildScenarioConfig([node("a", "Condenser", "cond", 0)], [], { controls });
    expect(cfg.controls).toEqual(controls);

    // No controls -> the field is omitted (stays a clean v1-compatible document).
    const bare = buildScenarioConfig([node("a", "Condenser", "cond", 0)], []);
    expect(bare.controls).toBeUndefined();
  });

  it("exports visible controller nodes as controls, not physical components", () => {
    const nodes = [
      node("comp", "IsentropicCompressor", "comp", 0, { liquid_injection: true }),
      node("ctrl", "SaturatedController", "dgt_limiter", 0, {
        measure_component: "comp",
        measure_output: "temperature",
        actuator_component: "comp",
        actuator_factor: "injection",
        initial: 0.15,
        min: 0,
        max: 0.3,
        target: 330,
        gain: -0.5,
        band: 5,
        alpha: 0.002,
        objectives_json: JSON.stringify([
          {
            component: "comp",
            output: "temperature",
            setpoint: 365,
            gain: -0.05,
            combine: "min",
          },
        ]),
      }),
    ];

    const cfg = buildScenarioConfig(nodes, []);
    expect(cfg.circuits[0].components.map((c) => c.type)).toEqual(["IsentropicCompressor"]);
    expect(cfg.controls).toEqual([
      {
        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,
        objectives: [
          {
            component: "comp",
            output: "temperature",
            setpoint: 365,
            gain: -0.05,
            combine: "min",
          },
        ],
        alpha: 0.002,
      },
    ]);
  });
});

describe("validateConfig", () => {
  it("flags an empty diagram", () => {
    expect(validateConfig([], [])).toContain("Add at least one component.");
  });

  it("flags duplicate names within a circuit", () => {
    const nodes = [
      node("a", "Condenser", "dup", 0),
      node("b", "Evaporator", "dup", 0),
    ];
    const issues = validateConfig(nodes, []);
    expect(issues.some((i) => i.includes("Duplicate component name"))).toBe(true);
  });

  it("allows identical names in different circuits", () => {
    const nodes = [
      node("a", "Condenser", "hx", 0),
      node("b", "Evaporator", "hx", 1),
    ];
    const issues = validateConfig(nodes, []);
    expect(issues.some((i) => i.includes("Duplicate"))).toBe(false);
  });

  it("flags a missing required parameter", () => {
    // Condenser requires `ua`; omit it.
    const nodes = [node("a", "Condenser", "cond", 0, { t_sat_k: 320 })];
    const issues = validateConfig(nodes, []);
    expect(issues.some((i) => i.includes("required parameter"))).toBe(true);
  });

  it("flags an unknown component type", () => {
    const nodes = [node("a", "Nonexistent", "x", 0)];
    const issues = validateConfig(nodes, []);
    expect(issues.some((i) => i.includes("Unknown component type"))).toBe(true);
  });

  it("passes a valid single-component diagram without secondary ports", () => {
    // Compressors have no secondary ports — pure required-param check.
    const nodes = [
      node("a", "IsentropicCompressor", "comp", 0, {
        isentropic_efficiency: 0.7,
        t_cond_k: 320,
        t_evap_k: 280,
        superheat_k: 5,
      }),
    ];
    expect(validateConfig(nodes, [])).toHaveLength(0);
  });

  it("requires live secondary ports OR rating scalars for heat exchangers", () => {
    const bare = [node("e", "FloodedEvaporator", "evap", 0, { ua: 8000 })];
    expect(validateConfig(bare, []).some((i) => i.includes("secondary incomplete"))).toBe(true);

    // Rating mode: scalars only, no edges — must be allowed.
    const rating = [
      node("e", "FloodedEvaporator", "evap", 0, {
        ua: 8000,
        secondary_inlet_temp_c: 12,
        secondary_mass_flow_kg_s: 0.5,
        secondary_cp_j_per_kgk: 4180,
      }),
    ];
    expect(validateConfig(rating, []).some((i) => i.includes("secondary incomplete"))).toBe(false);

    // System mode: live secondary ports.
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, { ua: 8000 }),
      node("b", "BrineSource", "brine", 0, { p_set_bar: 2, temperature_c: 10, mass_flow_kg_s: 1.2 }),
      node("k", "BrineSink", "sink", 0, { p_back_bar: 2 }),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
      { id: "s2", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    expect(validateConfig(nodes, edges).some((i) => i.includes("secondary incomplete"))).toBe(
      false,
    );
  });

  it("flags quality_control as a DoF risk", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, { ua: 8000, quality_control: true }),
      node("b", "BrineSource", "brine", 0, { p_set_bar: 2, t_set_c: 10, m_flow_kg_s: 1.2 }),
      node("k", "BrineSink", "sink", 0, { p_back_bar: 2 }),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
      { id: "s2", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    expect(validateConfig(nodes, edges).some((i) => i.includes("quality_control"))).toBe(true);
  });
});

describe("EXV orifice Fixed/Free", () => {
  it("emits fix_opening=true and orifice_kv when Fixed opening", () => {
    const raw = {
      t_evap_k: 275.15,
      opening: 0.6,
      orifice_kv: 2e-6,
      [fixedFlagKey("opening")]: true,
    };
    const out = stripUiOnlyParams("IsenthalpicExpansionValve", raw);
    expect(out.orifice_kv).toBe(2e-6);
    expect(out.fix_opening).toBe(true);
    expect(out.opening).toBe(0.6);
  });

  it("emits fix_opening=false when opening is Free", () => {
    const raw = {
      opening: 0.5,
      orifice_kv: 1e-6,
      [fixedFlagKey("opening")]: false,
    };
    const out = stripUiOnlyParams("IsenthalpicExpansionValve", raw);
    expect(out.fix_opening).toBe(false);
  });

  it("omits blank orifice_kv so opening stays a no-op", () => {
    const out = stripUiOnlyParams("IsenthalpicExpansionValve", {
      opening: 1.0,
      orifice_kv: "",
    });
    expect(out.orifice_kv).toBeUndefined();
    expect(out.fix_opening).toBeUndefined();
  });
});

describe("boundary Fixed/Free (Modelica-style)", () => {
  it("emits fix_mass_flow=false when ṁ is Free", () => {
    const raw = {
      p_set_bar: 2,
      t_set_c: 12,
      m_flow_kg_s: 0.5,
      [fixedFlagKey("p_set_bar")]: true,
      [fixedFlagKey("t_set_c")]: true,
      [fixedFlagKey("m_flow_kg_s")]: false,
    };
    const out = stripUiOnlyParams("BrineSource", raw);
    expect(out.fix_mass_flow).toBe(false);
    expect(out.fix_pressure).toBe(true);
    expect(out.fix_temperature).toBe(true);
    expect(out.m_flow_kg_s).toBe(0.5);
  });

  it("omits Free sink T_out so CLI does not impose enthalpy", () => {
    const raw = {
      p_back_bar: 2,
      t_set_c: 7,
      [fixedFlagKey("p_back_bar")]: true,
      [fixedFlagKey("t_set_c")]: false,
    };
    const out = stripUiOnlyParams("BrineSink", raw);
    expect(out.t_set_c).toBeUndefined();
    expect(out.fix_temperature).toBe(false);
  });

  it("forces Modelica MassFlowSource emit: Free P when Fixed ṁ", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, { ua: 8000 }),
      node("b", "BrineSource", "brine", 0, {
        p_set_bar: 3,
        t_set_c: 12,
        m_flow_kg_s: 0.55,
        [fixedFlagKey("m_flow_kg_s")]: true,
        [fixedFlagKey("p_set_bar")]: true,
      }),
      node("k", "BrineSink", "sink", 0, {
        p_back_bar: 3,
        [fixedFlagKey("p_back_bar")]: true,
      }),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
      { id: "s2", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    const src = cfg.circuits[0].components.find((c) => c.name === "brine")!;
    expect(src.fix_mass_flow).toBe(true);
    expect(src.fix_pressure).toBe(false);
  });

  it("keeps Free ṁ on source without inventing sink T from a source ΔT", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, { ua: 8000 }),
      node("b", "BrineSource", "brine", 0, {
        p_set_bar: 3,
        t_set_c: 12,
        m_flow_kg_s: 0.5,
        [fixedFlagKey("m_flow_kg_s")]: false,
      }),
      node("k", "BrineSink", "sink", 0, {
        p_back_bar: 3,
        t_set_c: 7,
        [fixedFlagKey("t_set_c")]: true,
      }),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
      { id: "s2", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    const sink = cfg.circuits[0].components.find((c) => c.name === "sink")!;
    const src = cfg.circuits[0].components.find((c) => c.name === "brine")!;
    expect(src.fix_mass_flow).toBe(false);
    expect(sink.t_set_c).toBe(7);
    expect(sink.fix_temperature).toBe(true);
    expect(src).not.toHaveProperty("delta_t_k");
  });

  it("applyBoundaryFixSemantics leaves non-boundaries unchanged", () => {
    const out = applyBoundaryFixSemantics("Condenser", { ua: 1000 }, { ua: 1000 });
    expect(out).toEqual({ ua: 1000 });
  });
});

describe("caloporteur (secondary stream) resolution", () => {
  it("does not fold or absorb explicit secondary stream nodes", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, { ua: 8000 }),
      node("b", "BrineSource", "brine", 0, { temperature_c: 9, mass_flow_kg_s: 1.4 }),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
    ];
    const { overrides, absorbed } = resolveSecondaryStreams(nodes, edges);
    expect(absorbed.size).toBe(0);
    expect(overrides.size).toBe(0);
  });

  it("preserves a secondary sink connected to the exchanger outlet", () => {
    const nodes = [
      node("e", "Evaporator", "evap", 0, { ua: 6000 }),
      node("k", "BrineSink", "sink", 0, {}),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    const { absorbed } = resolveSecondaryStreams(nodes, edges);
    expect(absorbed.has("k")).toBe(false);
  });

  it("keeps caloporteur connections in the solver graph as explicit 4-port branches", () => {
    const nodes = [
      node("c", "IsentropicCompressor", "comp", 0),
      node("e", "Evaporator", "evap", 0, { ua: 6000 }),
      node("b", "BrineSource", "brine", 0, { temperature_c: 11, mass_flow_kg_s: 2 }),
      node("k", "BrineSink", "sink", 0, {}),
    ];
    const edges: Edge[] = [
      { id: "r1", source: "c", target: "e", sourceHandle: "outlet", targetHandle: "inlet" },
      { id: "s1", source: "b", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
      { id: "s2", source: "e", target: "k", sourceHandle: "secondary_outlet", targetHandle: "inlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    const circuit = cfg.circuits[0];

    const names = circuit.components.map((c) => c.name);
    expect(names).toContain("comp");
    expect(names).toContain("evap");
    expect(names).toContain("brine");
    expect(names).toContain("sink");

    expect(circuit.edges).toEqual([
      { from: "comp:outlet", to: "evap:inlet" },
      { from: "brine:outlet", to: "evap:secondary_inlet" },
      { from: "evap:secondary_outlet", to: "sink:inlet" },
    ]);
  });

  it("does not absorb non-boundary nodes wired to a secondary port", () => {
    const nodes = [
      node("e", "Evaporator", "evap", 0, { ua: 6000 }),
      node("p", "Pump", "pump", 0, {}),
    ];
    const edges: Edge[] = [
      { id: "s1", source: "p", target: "e", sourceHandle: "outlet", targetHandle: "secondary_inlet" },
    ];
    const { absorbed, overrides } = resolveSecondaryStreams(nodes, edges);
    expect(absorbed.has("p")).toBe(false);
    expect(overrides.has("e")).toBe(false);
    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.circuits[0].components.map((c) => c.name)).toContain("pump");
    expect(cfg.circuits[0].edges).toEqual([{ from: "pump:outlet", to: "evap:secondary_inlet" }]);
  });
});

describe("Modelica embeddings (Fixed / Free)", () => {
  it("pairs Fixed Probe Tsat with Free Z_UA into embeddings[] (not controls[])", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, {
        ua: 9000,
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
      node("p1", "Probe", "sst_probe", 0, {
        fluid: "R134a",
        tsat_c: 5.0,
        [fixedFlagKey("tsat_c")]: true,
      }),
    ];
    const edges: Edge[] = [
      { id: "pe", source: "e", target: "p1", sourceHandle: "outlet", targetHandle: "inlet" },
    ];
    const embeddings = buildModelEmbeddings(nodes, edges);
    expect(embeddings).toHaveLength(1);
    expect(embeddings[0]).toMatchObject({
      id: "emb_evap_z_ua",
      unknown: { component: "evap", factor: "z_ua", start: 1.0, min: 0.05 },
      equation: {
        component: "sst_probe",
        output: "saturationTemperature",
        value: 278.15,
      },
    });

    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.embeddings).toHaveLength(1);
    expect(cfg.controls).toBeUndefined();
    const comp = cfg.circuits[0].components[0];
    expect(comp[fixedFlagKey("z_ua")]).toBeUndefined();
    expect(comp.z_ua).toBe(1.0);
    // Legacy HX: ua is the nominal base — keep it when Free z_ua.
    expect(comp.ua).toBe(9000);
  });

  it("strips BPHX ua override when Z_UA is Free (geometry = UA_nominal)", () => {
    const nodes = [
      node("c", "BphxCondenser", "cond", 0, {
        n_plates: 40,
        ua: 2500,
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
      node("p1", "Probe", "sdt_probe", 0, {
        fluid: "R134a",
        tsat_c: 42.0,
        [fixedFlagKey("tsat_c")]: true,
      }),
    ];
    const edges: Edge[] = [
      { id: "pe", source: "c", target: "p1", sourceHandle: "inlet", targetHandle: "outlet" },
    ];
    const cfg = buildScenarioConfig(nodes, edges);
    expect(cfg.embeddings).toHaveLength(1);
    expect(cfg.circuits[0].components[0].ua).toBeUndefined();
  });

  it("fills missing required ua on FloodedEvaporator when Z_UA is Free", () => {
    const nodes = [
      node("e", "FloodedEvaporator", "evap", 0, {
        // ua intentionally missing / cleared in the canvas
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
      node("p1", "Probe", "sst_probe", 0, {
        fluid: "R134a",
        tsat_c: 5.0,
        [fixedFlagKey("tsat_c")]: true,
      }),
    ];
    const cfg = buildScenarioConfig(nodes, [
      { id: "pe", source: "e", target: "p1", sourceHandle: "outlet", targetHandle: "inlet" },
    ]);
    expect(cfg.circuits[0].components[0].ua).toBe(8000);
    expect(cfg.embeddings).toHaveLength(1);
  });

  it("emits no embedding when Z_UA stays Fixed even with a Probe present", () => {
    const nodes = [
      node("e", "Evaporator", "evap", 0, {
        ua: 6000,
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: true,
      }),
      node("p1", "Probe", "sst_probe", 0, {
        tsat_c: 5.0,
        [fixedFlagKey("tsat_c")]: true,
      }),
    ];
    expect(buildModelEmbeddings(nodes)).toHaveLength(0);
    expect(buildFixedFreeCalibrationControls(nodes)).toHaveLength(0);
  });

  it("emits nothing for a freed Z_UA with no matching Probe", () => {
    const nodes = [
      node("e", "Evaporator", "evap", 0, {
        ua: 6000,
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
    ];
    expect(buildModelEmbeddings(nodes)).toHaveLength(0);
  });

  it("rejects Free Z_UA without a Fixed Probe equation (Modelica balance)", () => {
    const nodes = [
      node("e", "BphxEvaporator", "evap", 0, {
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
    ];
    const issues = validateConfig(nodes, []);
    expect(issues.some((m) => m.includes("fixed=false") || m.includes("without an equation"))).toBe(
      true,
    );
  });

  it("accepts legacy Probe measure+target (°C → K)", () => {
    const nodes = [
      node("e", "BphxEvaporator", "evap", 0, {
        z_ua: 1.0,
        [fixedFlagKey("z_ua")]: false,
      }),
      node("p1", "Probe", "sst_probe", 0, {
        measure: "SST",
        target: 4.4,
        __fixed_target: true,
      }),
    ];
    const edges: Edge[] = [
      { id: "pe", source: "e", target: "p1", sourceHandle: "outlet", targetHandle: "inlet" },
    ];
    const embeddings = buildModelEmbeddings(nodes, edges);
    expect(embeddings).toHaveLength(1);
    expect(embeddings[0].equation.value).toBeCloseTo(277.55, 10);
  });
});
