//! Integration tests for JSON serialization/deserialization of systems
//!
//! Tests cover:
//! - Round-trip serialization (system → JSON → system)
//! - Topology preservation (nodes, edges, component types)
//! - Constraint and bounded variable preservation
//! - Thermal coupling preservation
//! - Version compatibility checks
//! - Backend validation
//! - File save/load round-trip
//! - Human-readable JSON format

use entropyk_components::{Compressor, FluidId, Port};
use entropyk_core::{CircuitId, Enthalpy, Pressure, ThermalConductance};
use entropyk_solver::{System, ThermalCoupling};
use serde_json::{json, Value};

/// Helper: create a minimal system with a single compressor component.
fn build_single_compressor_system() -> System {
    let mut system = System::new();

    let coefficients = entropyk_components::Ahri540Coefficients::new(
        0.85, 2.5, 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
    );

    let port_suction = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(2.0),
        Enthalpy::from_joules_per_kg(400000.0),
    );
    let port_discharge = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(10.0),
        Enthalpy::from_joules_per_kg(450000.0),
    );

    let disconnected = Compressor::new(
        coefficients,
        port_suction,
        port_discharge,
        2900.0,
        0.0001,
        0.85,
    )
    .expect("Failed to create compressor");

    let connected = disconnected
        .connect(
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(2.0),
                Enthalpy::from_joules_per_kg(400000.0),
            ),
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(10.0),
                Enthalpy::from_joules_per_kg(450000.0),
            ),
        )
        .expect("Failed to connect compressor");

    let node = system.add_component(Box::new(connected));
    system.register_component_name("compressor", node);

    system
}

/// Helper: create a system with two components in a 2-edge cycle,
/// plus a thermal coupling. CM1.4: a single edge between two 3-eq
/// compressors is over-constrained; the second edge makes the topology
/// under-constrained so serialization round-trips can finalize.
fn build_two_component_system() -> System {
    let mut system = System::new();

    let coefficients = entropyk_components::Ahri540Coefficients::new(
        0.85, 2.5, 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
    );

    // Create compressor
    let port_s = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(2.0),
        Enthalpy::from_joules_per_kg(400000.0),
    );
    let port_d = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(10.0),
        Enthalpy::from_joules_per_kg(450000.0),
    );
    let comp = Compressor::new(coefficients, port_s, port_d, 2900.0, 0.0001, 0.85)
        .expect("create compressor")
        .connect(
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(2.0),
                Enthalpy::from_joules_per_kg(400000.0),
            ),
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(10.0),
                Enthalpy::from_joules_per_kg(450000.0),
            ),
        )
        .expect("connect compressor");

    let node_comp = system.add_component(Box::new(comp));
    system.register_component_name("compressor", node_comp);

    // Create a second compressor (acting as condenser proxy)
    let coefficients2 = entropyk_components::Ahri540Coefficients::new(
        0.9, 3.0, 600.0, 1400.0, -1.5, 2.0, 700.0, 1700.0, -2.0, 1.5,
    );
    let port_s2 = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(10.0),
        Enthalpy::from_joules_per_kg(450000.0),
    );
    let port_d2 = Port::new(
        FluidId::new("R134a"),
        Pressure::from_bar(8.0),
        Enthalpy::from_joules_per_kg(420000.0),
    );
    let comp2 = Compressor::new(coefficients2, port_s2, port_d2, 2900.0, 0.00012, 0.88)
        .expect("create comp2")
        .connect(
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(10.0),
                Enthalpy::from_joules_per_kg(450000.0),
            ),
            Port::new(
                FluidId::new("R134a"),
                Pressure::from_bar(8.0),
                Enthalpy::from_joules_per_kg(420000.0),
            ),
        )
        .expect("connect comp2");

    let node_comp2 = system.add_component(Box::new(comp2));
    system.register_component_name("condenser", node_comp2);

    // Add two edges forming a cycle so the topology is not over-constrained.
    system
        .add_edge(node_comp, node_comp2)
        .expect("add edge comp->cond");
    system
        .add_edge(node_comp2, node_comp)
        .expect("add edge cond->comp");

    // Add thermal coupling
    let coupling = ThermalCoupling::new(
        CircuitId(0),
        CircuitId(0),
        ThermalConductance::from_watts_per_kelvin(500.0),
    );
    let _ = system.add_thermal_coupling(coupling);

    system
}

// ────────────────────────────────────────────────────────────────────────
// Test 1: Topology round-trip
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_topology_round_trip() {
    let original = build_two_component_system();

    let json_str = original.to_json_string().expect("Serialization failed");
    let restored = System::from_json_string(&json_str).expect("Deserialization failed");

    // Verify topology is identical
    assert_eq!(
        original.node_count(),
        restored.node_count(),
        "Node count mismatch"
    );
    assert_eq!(
        original.edge_count(),
        restored.edge_count(),
        "Edge count mismatch"
    );
    assert_eq!(
        original.thermal_coupling_count(),
        restored.thermal_coupling_count(),
        "Thermal coupling count mismatch"
    );

    // Verify component names are preserved (order-independent since deserialization sorts keys)
    let mut original_names: Vec<&str> = original.registered_component_names().collect();
    let mut restored_names: Vec<&str> = restored.registered_component_names().collect();
    original_names.sort();
    restored_names.sort();
    assert_eq!(original_names, restored_names, "Component names mismatch");

    // Verify component types via the JSON snapshot
    let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
    let params = parsed.get("parameters").expect("parameters field");
    assert!(params.get("compressor").is_some(), "compressor in params");
    assert!(params.get("condenser").is_some(), "condenser in params");
}

// ────────────────────────────────────────────────────────────────────────
// Test 3: Constraints preservation
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_constraints_preserved_in_round_trip() {
    use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};

    let mut system = build_single_compressor_system();

    // Add a constraint referencing the compressor
    let constraint = Constraint::new(
        ConstraintId::new("superheat_ctrl"),
        ComponentOutput::Superheat {
            component_id: "compressor".to_string(),
        },
        5.0,
    );
    system.add_constraint(constraint).expect("add constraint");
    assert_eq!(system.constraint_count(), 1);

    // Serialize
    let json_str = system.to_json_string().expect("Serialization failed");

    // Verify constraints are in the JSON
    let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
    let constraints = parsed.get("constraints").expect("constraints field");
    assert!(constraints.is_array());
    assert_eq!(constraints.as_array().unwrap().len(), 1);

    let c = &constraints.as_array().unwrap()[0];
    assert_eq!(c["id"], "superheat_ctrl");
    assert_eq!(c["component"], "compressor");
    assert_eq!(c["target"], 5.0);

    // Verify the constraint snapshot round-trips through serde
    let snapshot: entropyk_solver::SystemSnapshot =
        serde_json::from_str(&json_str).expect("snapshot parse");
    assert_eq!(snapshot.constraints.len(), 1);
    assert_eq!(snapshot.constraints[0].id, "superheat_ctrl");
    assert_eq!(snapshot.constraints[0].component, "compressor");
    assert!((snapshot.constraints[0].target - 5.0).abs() < 1e-12);
}

// ────────────────────────────────────────────────────────────────────────
// Test 4: Thermal couplings preservation
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_thermal_couplings_preserved_in_round_trip() {
    let original = build_two_component_system();

    let json_str = original.to_json_string().expect("Serialization failed");

    // Verify thermal couplings in JSON
    let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");
    let couplings = parsed
        .get("topology")
        .and_then(|t| t.get("thermalCouplings"))
        .expect("thermal couplings in topology");
    assert!(couplings.is_array());
    assert_eq!(couplings.as_array().unwrap().len(), 1);

    let c = &couplings.as_array().unwrap()[0];
    assert_eq!(c["hotCircuit"], 0);
    assert_eq!(c["coldCircuit"], 0);

    // Verify the snapshot round-trips
    let snapshot: entropyk_solver::SystemSnapshot =
        serde_json::from_str(&json_str).expect("snapshot parse");
    assert_eq!(snapshot.topology.thermal_couplings.len(), 1);
    assert_eq!(
        snapshot.topology.thermal_couplings[0].hot_circuit,
        CircuitId(0)
    );
    assert_eq!(
        snapshot.topology.thermal_couplings[0].cold_circuit,
        CircuitId(0)
    );

    // Verify ua value round-trip
    let ua_val = snapshot.topology.thermal_couplings[0]
        .ua
        .to_watts_per_kelvin();
    assert!(
        (ua_val - 500.0).abs() < 1e-6,
        "UA value mismatch: {}",
        ua_val
    );
}

// ────────────────────────────────────────────────────────────────────────
// Test 5: File save/load round-trip
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_file_save_and_load() {
    let system = build_two_component_system();

    let temp_dir = std::env::temp_dir();
    let file_path = temp_dir.join("entropyk_test_round_trip.json");

    // Save
    system.save_json(&file_path).expect("Save failed");
    assert!(file_path.exists());

    // Load
    let loaded = System::load_json(&file_path).expect("Load failed");

    // Verify topology matches
    assert_eq!(system.node_count(), loaded.node_count());
    assert_eq!(system.edge_count(), loaded.edge_count());
    assert_eq!(
        system.thermal_coupling_count(),
        loaded.thermal_coupling_count()
    );

    // Clean up
    std::fs::remove_file(&file_path).ok();
}

// ────────────────────────────────────────────────────────────────────────
// Test 6: Missing backend error
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_missing_backend_returns_error() {
    // AC5: missing backend must produce an explicit error
    let json_with_unknown_backend = json!({
        "version": "1.0",
        "topology": {
            "edges": [],
            "thermalCouplings": []
        },
        "parameters": {},
        "fluidBackend": {
            "name": "NonExistentBackend",
            "version": "99.0.0"
        }
    })
    .to_string();

    let result = System::from_json_string(&json_with_unknown_backend);
    assert!(
        result.is_err(),
        "Should fail with BackendUnavailable for unknown backend"
    );
}

// ────────────────────────────────────────────────────────────────────────
// Test 7: Version mismatch error
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_version_mismatch() {
    let json_with_wrong_version = json!({
        "version": "99.0",
        "topology": {
            "edges": [],
            "thermalCouplings": []
        },
        "parameters": {},
        "fluidBackend": {
            "name": "TestBackend",
            "version": "1.0.0",
            "hash": "abc123"
        }
    })
    .to_string();

    let result = System::from_json_string(&json_with_wrong_version);
    assert!(result.is_err(), "Should fail with version mismatch");
}

// ────────────────────────────────────────────────────────────────────────
// Additional: JSON human-readable and deterministic
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_simple_system_round_trip() {
    let system = build_single_compressor_system();
    let json_str = system.to_json_string().expect("Serialization failed");

    let parsed: Value = serde_json::from_str(&json_str).expect("JSON parsing failed");
    assert!(parsed.is_object());
    assert_eq!(parsed["version"], "1.0");

    // Single isolated component should fail finalize during deserialization
    let result = System::from_json_string(&json_str);
    assert!(result.is_err(), "Isolated node should fail deserialization");
}

#[test]
fn test_json_is_human_readable() {
    let system = System::new();
    let json_str = system.to_json_string().expect("Serialization failed");

    assert!(json_str.contains('\n'));
    assert!(json_str.contains("  "));

    let _: Value = serde_json::from_str(&json_str).expect("Should be valid JSON");
}

#[test]
fn test_deterministic_serialization() {
    // Note: HashMap-based fields (parameters, ComponentParams) may produce
    // different key ordering across serializations, so we compare parsed
    // JSON values rather than raw strings.
    let system = build_single_compressor_system();
    let json1 = system.to_json_string().expect("Serialization failed");
    let json2 = system.to_json_string().expect("Serialization failed");

    let val1: Value = serde_json::from_str(&json1).expect("parse json1");
    let val2: Value = serde_json::from_str(&json2).expect("parse json2");
    assert_eq!(
        val1, val2,
        "Same system should produce identical JSON (structurally)"
    );
}

// ────────────────────────────────────────────────────────────────────────
// Test: Bounded variables in snapshot
// ────────────────────────────────────────────────────────────────────────

#[test]
fn test_bounded_variables_in_snapshot() {
    use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};

    let mut system = build_single_compressor_system();

    let valve = BoundedVariable::with_component(
        BoundedVariableId::new("valve"),
        "compressor",
        0.5,
        0.0,
        1.0,
    )
    .expect("create bounded var");
    system.add_bounded_variable(valve).expect("add bounded var");

    let json_str = system.to_json_string().expect("Serialization failed");
    let parsed: Value = serde_json::from_str(&json_str).expect("JSON parse");

    let bounded = parsed
        .get("boundedVariables")
        .expect("boundedVariables field");
    assert!(bounded.is_array());
    assert_eq!(bounded.as_array().unwrap().len(), 1);

    let bv = &bounded.as_array().unwrap()[0];
    assert_eq!(bv["id"], "valve");
    assert_eq!(bv["component"], "compressor");
    assert!((bv["initialValue"].as_f64().unwrap() - 0.5).abs() < 1e-12);

    // Verify snapshot round-trip
    let snapshot: entropyk_solver::SystemSnapshot =
        serde_json::from_str(&json_str).expect("snapshot parse");
    assert_eq!(snapshot.bounded_variables.len(), 1);
    assert_eq!(snapshot.bounded_variables[0].id, "valve");
    assert_eq!(snapshot.bounded_variables[0].component, "compressor");
    assert!((snapshot.bounded_variables[0].initial_value - 0.5).abs() < 1e-12);
    assert!((snapshot.bounded_variables[0].lower_bound - 0.0).abs() < 1e-12);
    assert!((snapshot.bounded_variables[0].upper_bound - 1.0).abs() < 1e-12);
}
