//! Domain-grid Jacobian-health CI gate (Story 0.5 / Epic 0).
//!
//! Epic-0 P0 surfaces must be FD-healthy. Deferred debt (NFR9 exchanger FD,
//! python stubs, screw incomplete J, sat_domain P1, bphx P2) is **not** probed
//! here — see [`EPIC0_ALLOW_LIST`] (empty) and `docs/audits/jacobian-health-report.md`.
//!
//! Regenerate the committed report after grid changes:
//! `cargo test -p entropyk-components --test jacobian_health_sweep -- --nocapture`
//! then manually sync `docs/audits/jacobian-health-report.md`.
#![allow(clippy::const_is_empty)]

use std::sync::Arc;

use entropyk_components::heat_exchanger::two_phase_dp::{
    friedel_multiplier, homogeneous_density, msh_gradient, FriedelInput,
};
use entropyk_components::heat_exchanger::Condenser;
use entropyk_components::jacobian_fd::{assert_jacobian_healthy, AllowListEntry, JacobianFdConfig};
use entropyk_components::valve_flow::{
    valve_mass_flow, valve_mass_flow_dp_down, valve_mass_flow_dp_up, ValveFlowInput, ValveFlowModel,
};
use entropyk_components::{Component, IsenthalpicExpansionValve};
use entropyk_core::CalibIndices;
use entropyk_fluids::TestBackend;

/// Epic-0 P0 allow-list: intentionally empty (must stay clean).
/// Deferred debt is documented in the health report, not suppressed here.
const EPIC0_ALLOW_LIST: &[AllowListEntry] = &[];

/// Narrow FD step for C¹ smooth_clamp neighborhoods.
const EDGE_CFG: JacobianFdConfig = JacobianFdConfig {
    rel_epsilon: 1e-6,
    h_floor: 1e-6,
    rel_tol: 1e-4,
    analytic_atol: 1e-12,
    fd_informative: 1e-8,
};

const DEFAULT_CFG: JacobianFdConfig = JacobianFdConfig {
    rel_epsilon: 1e-6,
    h_floor: 1e-3,
    rel_tol: 1e-4,
    analytic_atol: 1e-12,
    fd_informative: 1e-8,
};

// ── Region grids (Story 0.5) ────────────────────────────────────────────────

const EXV_OPENINGS: [f64; 3] = [0.0, 0.5, 1.0];
/// (p_in, p_out) — positive ΔP and mild ΔP≤0.
const EXV_DP_REGIMES: [(&str, f64, f64); 2] =
    [("dp_positive", 1.2e6, 3.5e5), ("dp_le_0", 4.0e5, 4.005e5)];

const CONDENSER_FAN_PHI: [f64; 3] = [0.005, 0.75, 1.495];
const CONDENSER_FLOOD_LAMBDA: [f64; 3] = [0.005, 0.5, 0.975];

const QUALITY_INTERIOR: f64 = 0.5;
const QUALITY_NEAR_BAND: f64 = 0.005;
const QUALITY_EXTERIOR: f64 = -0.2;

const MSH_QUALITY_GRID: [f64; 7] = [0.5, 0.8, 0.90, 0.95, 0.99, 0.999, 1.0 - 1e-9];

// ── Valve flow helpers ──────────────────────────────────────────────────────

fn valve_input(model_hint: &str, dp_positive: bool, opening: f64) -> ValveFlowInput {
    let (p_up, p_dn) = if dp_positive {
        (1.5e6, 0.4e6)
    } else {
        (4.0e5, 4.005e5)
    };
    let _ = model_hint;
    ValveFlowInput {
        density_kg_m3: 1200.0,
        p_upstream_pa: p_up,
        p_downstream_pa: p_dn,
        opening,
        p_bulb_pa: 0.0,
    }
}

#[test]
fn valve_flow_domain_grid_is_healthy() {
    let models: [(&str, ValveFlowModel); 2] = [
        (
            "isenthalpic_orifice",
            ValveFlowModel::IsenthalpicOrifice { beta_m2: 1.0e-6 },
        ),
        (
            "exv_cda",
            ValveFlowModel::ExvCdA {
                cd: 0.65,
                area_max_m2: 5e-6,
            },
        ),
    ];

    for (model_id, model) in &models {
        for dp_positive in [true, false] {
            for opening in [0.0, 0.5, 1.0] {
                let input = valve_input(model_id, dp_positive, opening);
                let m = valve_mass_flow(model, &input).expect("valve mass flow");
                assert!(
                    m.is_finite() && m >= 0.0,
                    "{model_id} opening={opening} dp+:{dp_positive}: ṁ={m}"
                );

                let d_up = valve_mass_flow_dp_up(model, &input).expect("dp_up");
                let d_dn = valve_mass_flow_dp_down(model, &input).expect("dp_down");
                assert!(
                    d_up.is_finite() && d_dn.is_finite(),
                    "{model_id}: non-finite derivatives"
                );

                // Opening ≈ 0 ⇒ ṁ≈0: pressure derivatives may be ~0 (not a ΔP killer).
                // Interior / open valve: informative ∂ṁ/∂P_* required.
                let opening_live = opening >= 0.05;
                if opening_live {
                    if !dp_positive {
                        assert!(
                            d_up > 0.0,
                            "{model_id} ΔP≤0 opening={opening}: expected phantom dmdp>0, got {d_up}"
                        );
                    } else {
                        assert!(
                            d_up > 0.0,
                            "{model_id} ΔP>0 opening={opening}: expected dmdp>0, got {d_up}"
                        );
                        let eps = 1.0;
                        let mut up = input;
                        up.p_downstream_pa += eps;
                        let mut dn = input;
                        dn.p_downstream_pa -= eps;
                        let d_fd = (valve_mass_flow(model, &up).unwrap()
                            - valve_mass_flow(model, &dn).unwrap())
                            / (2.0 * eps);
                        let scale = d_dn.abs().max(d_fd.abs()).max(1e-12);
                        assert!(
                            (d_dn - d_fd).abs() / scale < 1e-4,
                            "{model_id} opening={opening}: dp_down analytic {d_dn} vs FD {d_fd}"
                        );
                    }
                }
            }
        }
    }
}

// ── Two-phase quality helpers ───────────────────────────────────────────────

#[test]
fn two_phase_quality_domain_grid_is_healthy() {
    let rho_l = 1000.0;
    let rho_g = 50.0;
    let base = FriedelInput {
        quality: QUALITY_INTERIOR,
        mass_flux: 200.0,
        diameter: 0.01,
        rho_liquid: rho_l,
        rho_vapor: rho_g,
        mu_liquid: 2e-4,
        mu_vapor: 1e-5,
        sigma: 0.01,
    };

    // Interior control.
    let soft_int = homogeneous_density(QUALITY_INTERIOR, rho_l, rho_g);
    let hard_int = 1.0 / (QUALITY_INTERIOR / rho_g + (1.0 - QUALITY_INTERIOR) / rho_l);
    assert!(
        (soft_int - hard_int).abs() / hard_int < 1e-6,
        "interior quality must match hard formula; soft={soft_int} hard={hard_int}"
    );
    let m_int = friedel_multiplier(&base);
    assert!(m_int.is_finite() && m_int >= 1.0);

    // Near C¹ band: soft ≠ hard identity.
    let soft_band = homogeneous_density(QUALITY_NEAR_BAND, rho_l, rho_g);
    let hard_band = 1.0 / (QUALITY_NEAR_BAND / rho_g + (1.0 - QUALITY_NEAR_BAND) / rho_l);
    assert!(
        (soft_band - hard_band).abs() / hard_band > 1e-6,
        "near-band C¹ ramp must differ from hard clamp"
    );
    let m_band = friedel_multiplier(&FriedelInput {
        quality: QUALITY_NEAR_BAND,
        ..base
    });
    assert!(m_band.is_finite() && m_band > 0.0);

    // Exterior saturates to bound (smooth_clamp hard exterior).
    assert_eq!(
        homogeneous_density(QUALITY_EXTERIOR, rho_l, rho_g),
        homogeneous_density(0.0, rho_l, rho_g)
    );
    let m_ext = friedel_multiplier(&FriedelInput {
        quality: QUALITY_EXTERIOR,
        ..base
    });
    let m_0 = friedel_multiplier(&FriedelInput {
        quality: 0.0,
        ..base
    });
    assert!(
        (m_ext - m_0).abs() < 1e-12 * (1.0 + m_0.abs()),
        "exterior quality must saturate like x=0"
    );

    // Upper near-band + exterior > 1.
    let soft_hi = homogeneous_density(0.995, rho_l, rho_g);
    assert!(soft_hi.is_finite() && soft_hi > 0.0);
    assert_eq!(
        homogeneous_density(1.2, rho_l, rho_g),
        homogeneous_density(1.0, rho_l, rho_g)
    );
}

// ── MSH / dome edge ─────────────────────────────────────────────────────────

#[test]
fn msh_dome_edge_domain_grid_is_healthy() {
    let base = FriedelInput {
        quality: 0.5,
        mass_flux: 200.0,
        diameter: 0.01,
        rho_liquid: 1000.0,
        rho_vapor: 50.0,
        mu_liquid: 2e-4,
        mu_vapor: 1e-5,
        sigma: 0.01,
    };
    let h = 1e-6;
    for &x in &MSH_QUALITY_GRID {
        let g = msh_gradient(&FriedelInput { quality: x, ..base });
        assert!(g.is_finite() && g > 0.0, "MSH g({x})={g}");

        let mut up = base;
        up.quality = (x + h).min(1.0);
        let mut dn = base;
        dn.quality = (x - h).max(0.0);
        let denom = up.quality - dn.quality;
        if denom > 0.0 {
            let d_fd = (msh_gradient(&up) - msh_gradient(&dn)) / denom;
            assert!(
                d_fd.is_finite(),
                "MSH ∂g/∂x must be finite at x={x}, got {d_fd}"
            );
            // Near x→1 the Hermite blend forces slope → 0; elsewhere bounded.
            assert!(d_fd.abs() < 1e6, "MSH ∂g/∂x blew up at x={x}: {d_fd}");
        }
    }
}

// ── EXV orifice actuator ────────────────────────────────────────────────────

fn orifice_exv() -> IsenthalpicExpansionValve {
    let mut exv = IsenthalpicExpansionValve::new(275.15)
        .with_refrigerant("R134a")
        .with_orifice(3.0e-6)
        .with_fluid_backend(Arc::new(TestBackend::new()));
    exv.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
    exv.set_calib_indices(CalibIndices {
        actuator: Some(6),
        ..Default::default()
    });
    exv
}

fn orifice_state(opening: f64, p_in: f64, p_out: f64) -> Vec<f64> {
    vec![
        0.2,   // m_in
        p_in,  // p_in
        2.0e5, // h_in
        0.2,   // m_out
        p_out, // p_out
        2.0e5, // h_out
        opening,
    ]
}

#[test]
fn exv_orifice_domain_grid_is_jacobian_healthy() {
    let exv = orifice_exv();
    for opening in EXV_OPENINGS {
        for (region, p_in, p_out) in EXV_DP_REGIMES {
            let state = orifice_state(opening, p_in, p_out);
            let region_id = format!("{region}_opening_{opening}");
            // Opening edges need narrow h; interior + ΔP≤0 can use default floor
            // but EDGE_CFG is safe everywhere for this actuator scale.
            assert_jacobian_healthy(
                &exv,
                &state,
                EDGE_CFG,
                "exv_orifice",
                &region_id,
                EPIC0_ALLOW_LIST,
            );
        }
    }
}

/// Thin solve smoke: orifice residual at ΔP≤0 has informative pressure coupling
/// so a single Newton pressure step can restore ΔP>0 (no hard zero-gradient stall).
#[test]
fn exv_nonpositive_dp_newton_step_restores_positive_dp() {
    let exv = orifice_exv();
    let mut state = orifice_state(0.5, 4.0e5, 4.005e5);
    let mut r = vec![0.0; 3];
    exv.compute_residuals(&state, &mut r).unwrap();

    let mut jb = entropyk_components::JacobianBuilder::new();
    exv.jacobian_entries(&state, &mut jb).unwrap();
    let mut j_p_in = 0.0;
    let mut j_p_out = 0.0;
    for &(row, col, val) in jb.entries() {
        if row == 2 && col == 1 {
            j_p_in += val;
        }
        if row == 2 && col == 4 {
            j_p_out += val;
        }
    }
    assert!(
        j_p_in.abs() > 0.0 && j_p_out.abs() > 0.0,
        "pressure couplings must be live at ΔP≤0: ∂r/∂P_in={j_p_in}, ∂r/∂P_out={j_p_out}"
    );

    let step = 0.1 * r[2];
    if j_p_in.abs() > 1e-30 {
        state[1] -= step / j_p_in;
    }
    if j_p_out.abs() > 1e-30 {
        state[4] -= step / j_p_out;
    }
    let dp_after = state[1] - state[4];
    assert!(
        dp_after > -500.0,
        "Newton pressure step should not deepen reverse ΔP; got {dp_after}"
    );
}

// ── Condenser flooded / fan actuators ───────────────────────────────────────

#[test]
fn condenser_flood_domain_grid_is_jacobian_healthy() {
    let backend = Arc::new(TestBackend::new());
    let edges = [(0usize, 1usize, 2usize), (3usize, 4usize, 5usize)];
    let p_cond = 1_200_000.0_f64;

    let mut cond = Condenser::new(10_000.0)
        .with_refrigerant("R134a")
        .with_fluid_backend(backend)
        .with_secondary_stream(305.0, 3000.0)
        .with_emergent_pressure(0.0)
        .with_flooded_head_pressure(320.0);
    cond.set_system_context(0, &edges);
    cond.set_calib_indices(CalibIndices {
        actuator: Some(6),
        ..Default::default()
    });

    for lambda in CONDENSER_FLOOD_LAMBDA {
        let state = vec![0.1, p_cond, 440_000.0, 0.1, p_cond, 260_000.0, lambda];
        let region_id = format!("flood_lambda_{lambda}");
        assert_jacobian_healthy(
            &cond,
            &state,
            EDGE_CFG,
            "condenser_flood",
            &region_id,
            EPIC0_ALLOW_LIST,
        );
    }
}

#[test]
fn condenser_fan_domain_grid_is_jacobian_healthy() {
    let backend = Arc::new(TestBackend::new());
    let edges = [(0usize, 1usize, 2usize), (3usize, 4usize, 5usize)];
    let p_cond = 1_200_000.0_f64;

    let mut cond = Condenser::new(10_000.0)
        .with_refrigerant("R134a")
        .with_fluid_backend(backend)
        .with_secondary_stream(305.0, 3000.0)
        .with_emergent_pressure(0.0)
        .with_fan_head_pressure(320.0);
    cond.set_system_context(0, &edges);
    cond.set_calib_indices(CalibIndices {
        actuator: Some(6),
        ..Default::default()
    });

    for phi in CONDENSER_FAN_PHI {
        let state = vec![0.1, p_cond, 440_000.0, 0.1, p_cond, 260_000.0, phi];
        let region_id = format!("fan_phi_{phi}");
        assert_jacobian_healthy(
            &cond,
            &state,
            EDGE_CFG,
            "condenser_fan",
            &region_id,
            EPIC0_ALLOW_LIST,
        );
    }
}

#[test]
fn epic0_allow_list_is_empty_for_p0_surfaces() {
    // Guard: Story 0.5 forbids allow-listing valve/EXV/HX P0 regressions.
    assert!(
        EPIC0_ALLOW_LIST.is_empty(),
        "Epic-0 P0 allow-list must stay empty; deferred debt is documented, not suppressed"
    );
    let _ = DEFAULT_CFG; // keep default config linked for report documentation
}
