//! Phase-0 golden snapshot regression test.
//!
//! Solves three reference R134a cycles and compares the converged state against
//! committed snapshots. The comparison uses a SHA-256 hash of a canonical JSON
//! representation as the primary gate, plus a floating-point tolerant diff on
//! the fluid state vector for actionable diagnostics when the hash drifts.
//!
//! Run with `ENTROPYK_BLESS=1` to regenerate snapshots after an intentional
//! physics or solver change.

use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;

use approx::relative_eq;
use serde_json::Value;
use sha2::{Digest, Sha256};

mod common;

const BLESS_VAR: &str = "ENTROPYK_BLESS";
const SNAPSHOT_DIR: &str = "tests/snapshots";

struct Snapshot {
    value: Value,
    hash: String,
}

fn snapshot_path(name: &str) -> PathBuf {
    PathBuf::from(SNAPSHOT_DIR).join(format!("golden_{}.json", name))
}

fn load_snapshot(name: &str) -> Option<Snapshot> {
    let path = snapshot_path(name);
    if !path.exists() {
        return None;
    }
    let content = fs::read_to_string(&path).expect("failed to read snapshot");
    let value: Value = serde_json::from_str(&content).expect("invalid snapshot JSON");
    let hash = sha256_hex(&content);
    Some(Snapshot { value, hash })
}

fn sha256_hex(data: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data.as_bytes());
    format!("{:x}", hasher.finalize())
}

/// Recursively sorts object keys so the JSON representation is canonical and
/// hash-stable for the golden snapshot.
fn canonicalize(value: Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut sorted = BTreeMap::new();
            for (k, v) in map {
                sorted.insert(k, canonicalize(v));
            }
            Value::Object(sorted.into_iter().collect())
        }
        Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize).collect()),
        other => other,
    }
}

fn canonical_json(value: &Value) -> String {
    let canonical = canonicalize(value.clone());
    canonical.to_string()
}

fn extract_fluid_state(value: &Value) -> Option<&Vec<Value>> {
    value
        .get("fluidState")
        .and_then(|v| v.as_array())
        .or_else(|| value.get("fluid_state").and_then(|v| v.as_array()))
}

fn format_state_diff(expected: &[Value], actual: &[Value]) -> String {
    let mut lines = vec!["Fluid-state diff (index | expected | actual | rel_diff):".to_string()];
    let len = expected.len().max(actual.len());
    for i in 0..len {
        let e = expected.get(i).and_then(|v| v.as_f64());
        let a = actual.get(i).and_then(|v| v.as_f64());
        match (e, a) {
            (Some(e), Some(a)) => {
                let rel = if e.abs() > 0.0 {
                    ((a - e) / e).abs()
                } else {
                    a.abs()
                };
                lines.push(format!("  [{:02}] {:>18.6} {:>18.6} {:.6e}", i, e, a, rel));
            }
            (Some(e), None) => {
                lines.push(format!("  [{:02}] {:>18.6} {:>18} missing", i, e, "-"));
            }
            (None, Some(a)) => {
                lines.push(format!("  [{:02}] {:>18} {:>18.6} extra", i, "-", a));
            }
            (None, None) => {
                lines.push(format!("  [{:02}] {:>18} {:>18} ???", i, "-", "-"));
            }
        }
    }
    lines.join("\n")
}

fn assert_state_matches(name: &str, expected_value: &Value, actual_value: &Value) {
    let expected_state = extract_fluid_state(expected_value).expect("snapshot missing fluidState");
    let actual_state = extract_fluid_state(actual_value).expect("current solve missing fluidState");

    assert_eq!(
        expected_state.len(),
        actual_state.len(),
        "{}: fluid state length mismatch (expected {}, got {})",
        name,
        expected_state.len(),
        actual_state.len()
    );

    let mut mismatches = Vec::new();
    for (i, (e, a)) in expected_state.iter().zip(actual_state.iter()).enumerate() {
        let e = e.as_f64().expect("snapshot state value is not a number");
        let a = a.as_f64().expect("current state value is not a number");
        if !relative_eq!(e, a, epsilon = 1e-3, max_relative = 1e-5) {
            mismatches.push((i, e, a));
        }
    }

    if !mismatches.is_empty() {
        let mut msg = format!(
            "{}: {} fluid-state values exceed tolerance\n",
            name,
            mismatches.len()
        );
        msg.push_str(&format_state_diff(expected_state, actual_state));
        panic!("{}", msg);
    }
}

fn run_cycle_snapshot(name: &str, build_system: fn() -> entropyk_solver::system::System) {
    let mut system = build_system();
    let converged = common::solve_reference_system_with_state(&mut system);

    // The solver's returned state vector is the authoritative converged state.
    // System::to_json_string currently reads component ports, which are not
    // updated with the converged vector in this code path, so we merge the
    // solver state into the snapshot JSON manually.
    let json_str = system.to_json_string().expect("failed to serialize system");
    let mut actual_value: Value = serde_json::from_str(&json_str).expect("invalid system JSON");

    if let Some(Value::Object(obj)) = actual_value.get_mut("fluidState") {
        obj.insert("data".to_string(), converged.state.clone().into());
        obj.insert("edgeCount".to_string(), (converged.state.len() / 2).into());
    }

    let canonical = canonical_json(&actual_value);
    let actual_hash = sha256_hex(&canonical);

    let bless = env::var(BLESS_VAR).map(|v| v == "1").unwrap_or(false);

    if bless {
        fs::create_dir_all(SNAPSHOT_DIR).expect("failed to create snapshot directory");
        let path = snapshot_path(name);
        fs::write(&path, canonical).expect("failed to write snapshot");
        println!("📸 Blessed snapshot for {} -> {}", name, path.display());
        return;
    }

    let snapshot = load_snapshot(name).unwrap_or_else(|| {
        panic!(
            "No golden snapshot for {}. Run with {}=1 to create it.",
            name, BLESS_VAR
        )
    });

    if snapshot.hash != actual_hash {
        // Hash drifted; run tolerant comparison on fluid state for diagnostics.
        assert_state_matches(name, &snapshot.value, &actual_value);

        // If the fluid state is within tolerance but the hash changed, the drift
        // is in non-physics fields (topology, parameters, metadata). That is
        // still a regression for a golden snapshot.
        panic!(
            "{}: snapshot hash mismatch\n  expected: {}\n  actual:   {}\n  \
             The fluid state is within tolerance, but non-state fields changed. \
             If this is intentional, re-run with {}=1.",
            name, snapshot.hash, actual_hash, BLESS_VAR
        );
    }

    println!("✅ {} snapshot hash matches ({})", name, actual_hash);
}

#[test]
fn golden_reference_cycle_a() {
    run_cycle_snapshot("cycle_a", common::build_reference_cycle_a);
}

#[test]
fn golden_reference_cycle_b() {
    run_cycle_snapshot("cycle_b", common::build_reference_cycle_b);
}

#[test]
fn golden_reference_cycle_c() {
    run_cycle_snapshot("cycle_c", common::build_reference_cycle_c);
}
