//! Integration tests for structured simulation result extraction.
#![allow(clippy::doc_lazy_continuation)]

use entropyk::{extract_simulation_result, SimulationOutcome, SimulationResult, SystemBuilder};
use entropyk_components::expansion_valve::ExpansionValve;
use entropyk_components::heat_exchanger::Evaporator;
use entropyk_components::port::{Disconnected, FluidId, Port};
use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, MchxCondenserCoil, Polynomial2D,
    ResidualVector, ScrewEconomizerCompressor, ScrewPerformanceCurves,
};
use entropyk_core::{Enthalpy, Power, Pressure};
use entropyk_solver::{ConvergedState, ConvergenceStatus, SimulationMetadata};

use approx::assert_relative_eq;

// ─────────────────────────────────────────────────────────────────────────────
// Helpers for real components
// ─────────────────────────────────────────────────────────────────────────────

fn make_disconnected_port(fluid: &str, p_bar: f64, h_kj_kg: f64) -> Port<Disconnected> {
    Port::new(
        FluidId::new(fluid),
        Pressure::from_bar(p_bar),
        Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
    )
}

fn make_connected_port(fluid: &str, p_bar: f64, h_kj_kg: f64) -> ConnectedPort {
    let a = make_disconnected_port(fluid, p_bar, h_kj_kg);
    let b = make_disconnected_port(fluid, p_bar, h_kj_kg);
    a.connect(b).expect("port connection ok").0
}

fn make_screw_curves() -> ScrewPerformanceCurves {
    ScrewPerformanceCurves::with_fixed_eco_fraction(
        Polynomial2D::bilinear(1.20, 0.003, -0.002, 0.000_01),
        Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
        0.12,
    )
}

/// Build a real R134a cycle with ScrewEconomizerCompressor + MchxCondenserCoil
/// + ExpansionValve + Evaporator. Uses manually crafted ConvergedState from
/// NIST R134a reference data (T_evap=0°C, T_cond=40°C, SH=5K, SC=3K).
fn build_real_r134a_cycle() -> (entropyk_solver::System, ConvergedState) {
    use entropyk_solver::CircuitId;

    let mut sys = entropyk_solver::System::new();

    // --- Compressor (screw with economizer) ---
    let suc = make_connected_port("R134a", 2.93, 405.0);
    let dis = make_connected_port("R134a", 10.17, 440.0);
    let eco = make_connected_port("R134a", 5.5, 250.0);
    let comp =
        ScrewEconomizerCompressor::new(make_screw_curves(), "R134a", 50.0, 0.92, suc, dis, eco)
            .expect("compressor");

    // --- Condenser (air-cooled coil at 35°C ambient) ---
    let condenser = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);

    // --- Expansion valve (fully open) ---
    let exv_in = make_disconnected_port("R134a", 10.17, 253.4);
    let exv_out = make_disconnected_port("R134a", 2.93, 253.4);
    let exv_disconnected =
        ExpansionValve::new(exv_in, exv_out, Some(1.0)).expect("exv disconnected");
    let exv = exv_disconnected
        .connect(
            make_disconnected_port("R134a", 10.17, 253.4),
            make_disconnected_port("R134a", 2.93, 253.4),
        )
        .expect("exv connect");

    // --- Evaporator (BPHE, T_sat=278.15K, SH=5K) ---
    let evaporator = Evaporator::with_superheat(8000.0, 278.15, 5.0);

    // Add to circuit 0
    let n_comp = sys
        .add_component_to_circuit(Box::new(comp), CircuitId::ZERO)
        .unwrap();
    let n_cond = sys
        .add_component_to_circuit(Box::new(condenser), CircuitId::ZERO)
        .unwrap();
    let n_exv = sys
        .add_component_to_circuit(Box::new(exv), CircuitId::ZERO)
        .unwrap();
    let n_evap = sys
        .add_component_to_circuit(Box::new(evaporator), CircuitId::ZERO)
        .unwrap();

    // Register names for extract_simulation_result
    sys.register_component_name("compressor", n_comp);
    sys.register_component_name("condenser", n_cond);
    sys.register_component_name("expansion_valve", n_exv);
    sys.register_component_name("evaporator", n_evap);

    // Connect: comp → cond → exv → evap → comp
    sys.add_edge(n_comp, n_cond).unwrap();
    sys.add_edge(n_cond, n_exv).unwrap();
    sys.add_edge(n_exv, n_evap).unwrap();
    sys.add_edge(n_evap, n_comp).unwrap();

    // DoF gate escape hatch: the real components contribute 6+2+2+2 = 12 equations
    // vs 10 unknowns (1 branch ṁ + 4×(P,h) + compressor internal W_shaft) because no
    // free actuators (compressor speed, EXV opening) are registered here. This test
    // exercises result extraction/JSON serialization, not DoF balancing, so the
    // over-constrained system is accepted deliberately (test-only escape hatch).
    sys.set_enforce_dof_gate(false);
    sys.finalize().expect("system finalize");

    // ConvergedState from NIST R134a reference data:
    // T_evap_sat = 0°C → P_sat ≈ 292800 Pa (2.928 bar)
    // T_cond_sat = 40°C → P_sat ≈ 1017000 Pa (10.17 bar)
    // h_g(0°C) ≈ 398600 J/kg, h_f(40°C) ≈ 256400 J/kg
    // With SH=5K and SC=3K
    // CM1.4: 1 series branch + 4 × (P, h) = 9 elements.
    // [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
    let state = vec![
        0.05, // ṁ branch (shared, ~0.05 kg/s)
        1017000.0, 440000.0, // edge 0: comp→cond (discharge, superheated ~440 kJ/kg)
        1000000.0, 250000.0, // edge 1: cond→exv (subcooled liquid ~250 kJ/kg, ~3K SC)
        292800.0, 250000.0, // edge 2: exv→evap (isenthalpic expansion, same h)
        285000.0, 405000.0, // edge 3: evap→comp (superheated ~5K above sat)
    ];

    let converged = ConvergedState::new(
        state,
        23,
        5.1e-8,
        ConvergenceStatus::Converged,
        SimulationMetadata::new("r134a_chiller_nist_ref".to_string()),
    );

    (sys, converged)
}

// ─────────────────────────────────────────────────────────────────────────────
// Mock components for testing
// ─────────────────────────────────────────────────────────────────────────────

/// Mock component that reports energy transfers (simulates a compressor).
struct MockCompressor;

impl Component for MockCompressor {
    fn compute_residuals(
        &self,
        _state: &[f64],
        _residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _state: &[f64],
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn energy_transfers(&self, _state: &[f64]) -> Option<(Power, Power)> {
        // Compressor: no heat exchange, consumes 3000W work
        Some((Power::from_watts(0.0), Power::from_watts(3000.0)))
    }
    fn signature(&self) -> String {
        "Compressor(eff=0.7)".to_string()
    }
}

/// Mock component that absorbs heat (simulates an evaporator).
struct MockEvaporator;

impl Component for MockEvaporator {
    fn compute_residuals(
        &self,
        _state: &[f64],
        _residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _state: &[f64],
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn energy_transfers(&self, _state: &[f64]) -> Option<(Power, Power)> {
        // Evaporator: absorbs 10000W of heat (Q > 0 = cooling)
        Some((Power::from_watts(10000.0), Power::from_watts(0.0)))
    }
    fn signature(&self) -> String {
        "Evaporator(UA=5.0kW/K)".to_string()
    }
}

/// Mock component with no energy transfers (simulates a pipe).
struct MockPipe;

impl Component for MockPipe {
    fn compute_residuals(
        &self,
        _state: &[f64],
        _residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _state: &[f64],
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn signature(&self) -> String {
        "Pipe(L=10m,D=0.02m)".to_string()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

/// Helper: build a realistic 4-component vapor compression cycle with mock components.
#[allow(dead_code)] // Reusable cycle fixture for result-serialization tests.
fn build_realistic_cycle() -> (entropyk_solver::System, ConvergedState) {
    let system = SystemBuilder::new()
        .component("compressor", Box::new(MockCompressor))
        .expect("add compressor")
        .component("condenser", Box::new(MockPipe)) // no energy transfer
        .expect("add condenser")
        .component("expansion_valve", Box::new(MockPipe))
        .expect("add expansion valve")
        .component("evaporator", Box::new(MockEvaporator))
        .expect("add evaporator")
        .edge("compressor", "condenser")
        .expect("edge comp->cond")
        .edge("condenser", "expansion_valve")
        .expect("edge cond->exv")
        .edge("expansion_valve", "evaporator")
        .expect("edge exv->evap")
        .edge("evaporator", "compressor")
        .expect("edge evap->comp")
        .build()
        .expect("build system");

    // CM1.4 layout: 1 series branch + 4 × (P, h) = 9 elements
    // [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
    // Realistic R410A values: high side ~24 bar, low side ~8 bar
    let state = vec![
        0.05, // ṁ branch (shared)
        2400000.0,
        440000.0, // edge 0: compressor → condenser (discharge, high P, superheated)
        2350000.0, 280000.0, // edge 1: condenser → expansion (subcooled liquid)
        800000.0, 260000.0, // edge 2: expansion → evaporator (two-phase, low P)
        780000.0, 400000.0, // edge 3: evaporator → compressor (superheated vapor, low P)
    ];

    let converged = ConvergedState::new(
        state,
        12,
        2.3e-8,
        ConvergenceStatus::Converged,
        SimulationMetadata::new("r410a_chiller_35c_ambient".to_string()),
    );

    (system, converged)
}

/// Helper: build a 4-component system and create a fake ConvergedState.
fn build_test_system() -> (entropyk_solver::System, ConvergedState) {
    let system = SystemBuilder::new()
        .component("comp", Box::new(MockCompressor))
        .expect("add comp")
        .component("pipe1", Box::new(MockPipe))
        .expect("add pipe1")
        .component("evap", Box::new(MockEvaporator))
        .expect("add evap")
        .component("pipe2", Box::new(MockPipe))
        .expect("add pipe2")
        .edge("comp", "pipe1")
        .expect("edge comp->pipe1")
        .edge("pipe1", "evap")
        .expect("edge pipe1->evap")
        .edge("evap", "pipe2")
        .expect("edge evap->pipe2")
        .edge("pipe2", "comp")
        .expect("edge pipe2->comp")
        .build()
        .expect("build system");

    // CM1.4 layout: 1 series branch + 4 × (P, h) = 9 elements
    // [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
    let state = vec![
        0.05, // ṁ branch (shared)
        500000.0, 450000.0, // edge 0: comp -> pipe1 (high pressure)
        490000.0, 440000.0, // edge 1: pipe1 -> evap
        200000.0, 250000.0, // edge 2: evap -> pipe2 (low pressure)
        190000.0, 240000.0, // edge 3: pipe2 -> comp
    ];

    let converged = ConvergedState::new(
        state,
        15,
        1e-9,
        ConvergenceStatus::Converged,
        SimulationMetadata::new("test_input_hash".to_string()),
    );

    (system, converged)
}

#[test]
fn test_extract_simulation_result_basic() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    // Status and convergence
    assert_eq!(result.status, SimulationOutcome::Converged);
    assert!(result.convergence.converged);
    assert_eq!(result.convergence.iterations, 15);
    assert_relative_eq!(result.convergence.final_residual, 1e-9);

    // Components
    assert_eq!(result.components.len(), 4);

    // Edges
    assert_eq!(result.edges.len(), 4);
}

#[test]
fn test_extract_per_component_results() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    let comp = result
        .components
        .iter()
        .find(|c| c.name == "comp")
        .expect("comp not found");
    assert_eq!(comp.component_type, "Compressor(eff=0.7)");
    assert_eq!(comp.circuit, 0);
    assert!(comp.energy.is_some());
    let energy = comp.energy.as_ref().unwrap();
    assert_relative_eq!(energy.work_w, 3000.0);
    assert_relative_eq!(energy.heat_transfer_w, 0.0);

    let evap = result
        .components
        .iter()
        .find(|c| c.name == "evap")
        .expect("evap not found");
    assert_eq!(evap.component_type, "Evaporator(UA=5.0kW/K)");
    let evap_energy = evap.energy.as_ref().unwrap();
    assert_relative_eq!(evap_energy.heat_transfer_w, 10000.0);
    assert_relative_eq!(evap_energy.work_w, 0.0);

    // Pipe has no energy transfers
    let pipe1 = result
        .components
        .iter()
        .find(|c| c.name == "pipe1")
        .expect("pipe1 not found");
    assert!(pipe1.energy.is_none());
}

#[test]
fn test_extract_per_edge_results() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    // Edge 0: comp -> pipe1 (high pressure side)
    let edge0 = result
        .edges
        .iter()
        .find(|e| e.edge_id == 0)
        .expect("edge 0");
    assert_relative_eq!(edge0.pressure_pa, 500000.0);
    assert_relative_eq!(edge0.enthalpy_j_kg, 450000.0);
    assert_eq!(edge0.source.as_deref(), Some("comp"));
    assert_eq!(edge0.target.as_deref(), Some("pipe1"));

    // Edge 2: evap -> pipe2 (low pressure side)
    let edge2 = result
        .edges
        .iter()
        .find(|e| e.edge_id == 2)
        .expect("edge 2");
    assert_relative_eq!(edge2.pressure_pa, 200000.0);
    assert_relative_eq!(edge2.enthalpy_j_kg, 250000.0);
    assert_eq!(edge2.source.as_deref(), Some("evap"));
    assert_eq!(edge2.target.as_deref(), Some("pipe2"));
}

#[test]
fn test_system_summary() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    // Evaporator absorbs 10000W (cooling), compressor uses 3000W
    assert!(result.summary.total_cooling_capacity_w.is_some());
    assert_relative_eq!(result.summary.total_cooling_capacity_w.unwrap(), 10000.0);
    assert!(result.summary.total_compressor_power_w.is_some());
    assert_relative_eq!(result.summary.total_compressor_power_w.unwrap(), 3000.0);

    // COP_cooling = 10000 / 3000
    assert!(result.summary.cop_cooling.is_some());
    assert_relative_eq!(
        result.summary.cop_cooling.unwrap(),
        10000.0 / 3000.0,
        epsilon = 1e-10
    );
}

#[test]
fn test_simulation_result_json_roundtrip() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    let json = result.to_json().expect("to_json should succeed");
    assert!(json.contains("\"status\": \"converged\""));
    assert!(json.contains("\"iterations\": 15"));
    assert!(json.contains("Compressor"));
    assert!(json.contains("Evaporator"));

    // Round-trip (compare structurally; floats via relative_eq)
    let deserialized: SimulationResult =
        serde_json::from_str(&json).expect("deserialize should succeed");
    assert_eq!(result.status, deserialized.status);
    assert_eq!(
        result.convergence.iterations,
        deserialized.convergence.iterations
    );
    assert_relative_eq!(
        result.convergence.final_residual,
        deserialized.convergence.final_residual,
        epsilon = 1e-15
    );
    assert_eq!(
        result.convergence.converged,
        deserialized.convergence.converged
    );
    assert_eq!(result.convergence.status, deserialized.convergence.status);
    assert_eq!(result.components.len(), deserialized.components.len());
    assert_eq!(result.edges.len(), deserialized.edges.len());
    // Verify key float fields survive round-trip
    for (a, b) in result.edges.iter().zip(deserialized.edges.iter()) {
        assert_relative_eq!(a.pressure_pa, b.pressure_pa, epsilon = 1e-5);
        assert_relative_eq!(a.enthalpy_j_kg, b.enthalpy_j_kg, epsilon = 1e-5);
    }
}

#[test]
fn test_component_inlet_outlet_from_edges() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    // "comp" has: incoming edge 3 (pipe2->comp), outgoing edge 0 (comp->pipe1)
    let comp = result
        .components
        .iter()
        .find(|c| c.name == "comp")
        .expect("comp");

    // Inlet: edge 3 -> P=190000, h=240000
    assert!(comp.inlet.is_some());
    let inlet = comp.inlet.as_ref().unwrap();
    assert_relative_eq!(inlet.pressure_pa, 190000.0);
    assert_relative_eq!(inlet.enthalpy_j_kg, 240000.0);

    // Outlet: edge 0 -> P=500000, h=450000
    assert!(comp.outlet.is_some());
    let outlet = comp.outlet.as_ref().unwrap();
    assert_relative_eq!(outlet.pressure_pa, 500000.0);
    assert_relative_eq!(outlet.enthalpy_j_kg, 450000.0);
}

#[test]
fn test_metadata_preserved() {
    let (system, converged) = build_test_system();
    let result = extract_simulation_result(&system, &converged);

    assert_eq!(result.metadata.input_hash, "test_input_hash");
    assert!(!result.metadata.solver_version.is_empty());
}

#[test]
fn test_realistic_cycle_json_output() {
    let (system, converged) = build_real_r134a_cycle();
    let result = extract_simulation_result(&system, &converged);
    let json = result.to_json().expect("to_json");

    println!("\n{}", json);

    // Basic structure checks
    assert_eq!(result.status, SimulationOutcome::Converged);
    assert!(result.convergence.converged);
    assert_eq!(result.components.len(), 4);
    assert_eq!(result.edges.len(), 4);
    assert!(json.contains("\"compressor\""));
    assert!(json.contains("\"evaporator\""));
    assert!(json.contains("\"condenser\""));
    assert!(json.contains("\"expansion_valve\""));

    // Compressor should have real component type (not Mock)
    let comp = result
        .components
        .iter()
        .find(|c| c.name == "compressor")
        .expect("comp");
    assert!(
        comp.component_type.contains("Screw"),
        "expected ScrewEconomizer, got {}",
        comp.component_type
    );

    // Condenser should be MchxCondenserCoil
    let cond = result
        .components
        .iter()
        .find(|c| c.name == "condenser")
        .expect("cond");
    assert!(
        cond.component_type.contains("Mchx"),
        "expected MchxCondenserCoil, got {}",
        cond.component_type
    );

    // Expansion valve should have real type
    let exv = result
        .components
        .iter()
        .find(|c| c.name == "expansion_valve")
        .expect("exv");
    assert!(
        exv.component_type.contains("ExpansionValve"),
        "expected ExpansionValve, got {}",
        exv.component_type
    );

    // Evaporator
    let evap = result
        .components
        .iter()
        .find(|c| c.name == "evaporator")
        .expect("evap");
    assert!(
        evap.component_type.contains("Evaporator"),
        "expected Evaporator, got {}",
        evap.component_type
    );

    // Check edge pressures are from NIST data
    let edge0 = result.edges.iter().find(|e| e.edge_id == 0).unwrap();
    assert_relative_eq!(edge0.pressure_pa, 1017000.0);
    assert_eq!(edge0.source.as_deref(), Some("compressor"));
    assert_eq!(edge0.target.as_deref(), Some("condenser"));

    let edge2 = result.edges.iter().find(|e| e.edge_id == 2).unwrap();
    assert_relative_eq!(edge2.pressure_pa, 292800.0); // P_sat at 0°C (NIST)
    assert_eq!(edge2.source.as_deref(), Some("expansion_valve"));
    assert_eq!(edge2.target.as_deref(), Some("evaporator"));

    println!("\n=== Component types ===");
    for c in &result.components {
        println!("  {} (circuit {}): {}", c.name, c.circuit, c.component_type);
    }
}
