//! Cross-validation parity tests: verify Rust produces identical results to the JS reference.
//!
//! The expected values in this file were generated by running
//! `blast-stress-solver/scripts/generate-reference-data.mjs` against the JS implementation.
//! Both implementations use the same C++ Blast stress solver core via FFI.

#[cfg(feature = "scenarios")]
mod parity {
    use blast_stress_solver::scenarios::*;
    use blast_stress_solver::*;

    const EPS: f32 = 1e-4;

    fn approx_eq(a: f32, b: f32) -> bool {
        (a - b).abs() < EPS
    }

    // -------------------------------------------------------------------------
    // Scenario shape parity — node/bond counts must match JS exactly
    // -------------------------------------------------------------------------

    #[test]
    fn wall_default_shape_matches_js() {
        let wall = build_wall_scenario(&WallOptions::default());
        assert_eq!(wall.nodes.len(), 72, "JS: 72 nodes");
        assert_eq!(wall.bonds.len(), 126, "JS: 126 bonds");
        let support = wall.nodes.iter().filter(|n| n.mass == 0.0).count();
        let dynamic = wall.nodes.iter().filter(|n| n.mass > 0.0).count();
        assert_eq!(support, 12, "JS: 12 support nodes");
        assert_eq!(dynamic, 60, "JS: 60 dynamic nodes");
    }

    #[test]
    fn wall_default_centroids_match_js() {
        let wall = build_wall_scenario(&WallOptions::default());
        let first = &wall.nodes[0].centroid;
        let last = &wall.nodes[wall.nodes.len() - 1].centroid;
        // JS: first = (-2.75, 0.25, 0), last = (2.75, 2.75, 0)
        assert!(
            approx_eq(first.x, -2.75),
            "first.x = {} expected -2.75",
            first.x
        );
        assert!(
            approx_eq(first.y, 0.25),
            "first.y = {} expected 0.25",
            first.y
        );
        assert!(
            approx_eq(first.z, 0.0),
            "first.z = {} expected 0.0",
            first.z
        );
        assert!(approx_eq(last.x, 2.75), "last.x = {} expected 2.75", last.x);
        assert!(approx_eq(last.y, 2.75), "last.y = {} expected 2.75", last.y);
        assert!(approx_eq(last.z, 0.0), "last.z = {} expected 0.0", last.z);
    }

    #[test]
    fn wall_default_bond_areas_match_js() {
        let wall = build_wall_scenario(&WallOptions::default());
        // JS reference: 0.02157439559882373
        let js_area = 0.02157439559882373_f32;
        let first_area = wall.bonds[0].area;
        let last_area = wall.bonds[wall.bonds.len() - 1].area;
        assert!(
            (first_area - js_area).abs() < 1e-5,
            "first bond area = {first_area}, JS = {js_area}"
        );
        assert!(
            (last_area - js_area).abs() < 1e-5,
            "last bond area = {last_area}, JS = {js_area}"
        );
    }

    #[test]
    fn tower_default_shape_matches_js() {
        let tower = build_tower_scenario(&TowerOptions::default());
        assert_eq!(tower.nodes.len(), 144, "JS: 144 nodes");
        assert_eq!(tower.bonds.len(), 728, "JS: 728 bonds");
        let support = tower.nodes.iter().filter(|n| n.mass == 0.0).count();
        let dynamic = tower.nodes.iter().filter(|n| n.mass > 0.0).count();
        assert_eq!(support, 16, "JS: 16 support nodes");
        assert_eq!(dynamic, 128, "JS: 128 dynamic nodes");
    }

    #[test]
    fn bridge_default_shape_matches_js() {
        let bridge = build_bridge_scenario(&BridgeOptions::default());
        assert_eq!(bridge.nodes.len(), 908, "JS: 908 nodes");
        assert_eq!(bridge.bonds.len(), 3070, "JS: 3070 bonds");
        let support = bridge.nodes.iter().filter(|n| n.mass == 0.0).count();
        let dynamic = bridge.nodes.iter().filter(|n| n.mass > 0.0).count();
        assert_eq!(support, 28, "JS: 28 support nodes");
        assert_eq!(dynamic, 880, "JS: 880 dynamic nodes");
    }

    // -------------------------------------------------------------------------
    // Solver simulation parity — fracture/actor counts must match JS
    // -------------------------------------------------------------------------

    fn simulate(solver: &mut ExtStressSolver, gravity: Vec3, frames: u32) -> (u32, u32) {
        let mut total_fractures = 0u32;
        for _ in 0..frames {
            solver.add_gravity(gravity);
            solver.update();
            if solver.overstressed_bond_count() > 0 {
                let cmds = solver.generate_fracture_commands();
                let fractures: u32 = cmds.iter().map(|c| c.bond_fractures.len() as u32).sum();
                total_fractures += fractures;
                if !cmds.is_empty() {
                    solver.apply_fracture_commands(&cmds);
                }
            }
        }
        (total_fractures, solver.actor_count())
    }

    fn strong_settings() -> SolverSettings {
        SolverSettings {
            max_solver_iterations_per_frame: 24,
            compression_elastic_limit: 90_000.0,
            compression_fatal_limit: 270_000.0,
            tension_elastic_limit: 90_000.0,
            tension_fatal_limit: 270_000.0,
            shear_elastic_limit: 120_000.0,
            shear_fatal_limit: 360_000.0,
            ..SolverSettings::default()
        }
    }

    fn weak_settings() -> SolverSettings {
        SolverSettings {
            max_solver_iterations_per_frame: 24,
            compression_elastic_limit: 0.001,
            compression_fatal_limit: 0.002,
            tension_elastic_limit: 0.001,
            tension_fatal_limit: 0.002,
            shear_elastic_limit: 0.001,
            shear_fatal_limit: 0.002,
            ..SolverSettings::default()
        }
    }

    #[test]
    fn wall_strong_gravity_parity() {
        // JS reference: 0 fractures, 1 actor after 60 frames
        let wall = build_wall_scenario(&WallOptions::default());
        let (nodes, bonds) = wall.to_solver_descs();
        let mut solver = ExtStressSolver::new(&nodes, &bonds, &strong_settings()).unwrap();
        let (fractures, actors) = simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 60);
        assert_eq!(fractures, 0, "JS: 0 fractures (strong wall under gravity)");
        assert_eq!(actors, 1, "JS: 1 actor (no splits)");
    }

    #[test]
    fn wall_weak_gravity_parity() {
        // JS reference: 86 fractures, 32 actors after 60 frames
        let wall = build_wall_scenario(&WallOptions::default());
        let (nodes, bonds) = wall.to_solver_descs();
        let mut solver = ExtStressSolver::new(&nodes, &bonds, &weak_settings()).unwrap();
        let (fractures, actors) = simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 60);
        assert_eq!(fractures, 86, "JS: 86 fractures (weak wall under gravity)");
        assert_eq!(actors, 32, "JS: 32 actors after fracture");
    }

    #[test]
    fn tower_strong_gravity_parity() {
        // JS reference: 0 fractures, 1 actor after 60 frames
        let tower = build_tower_scenario(&TowerOptions::default());
        let (nodes, bonds) = tower.to_solver_descs();
        let mut solver = ExtStressSolver::new(&nodes, &bonds, &strong_settings()).unwrap();
        let (fractures, actors) = simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 60);
        assert_eq!(fractures, 0, "JS: 0 fractures (strong tower)");
        assert_eq!(actors, 1, "JS: 1 actor");
    }

    #[test]
    fn tower_weak_gravity_parity() {
        // JS reference: 706 fractures, 129 actors after 60 frames
        let tower = build_tower_scenario(&TowerOptions::default());
        let (nodes, bonds) = tower.to_solver_descs();
        let mut solver = ExtStressSolver::new(&nodes, &bonds, &weak_settings()).unwrap();
        let (fractures, actors) = simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 60);
        assert_eq!(fractures, 706, "JS: 706 fractures (weak tower)");
        assert_eq!(actors, 129, "JS: 129 actors");
    }

    // -------------------------------------------------------------------------
    // Determinism parity — two identical runs must match
    // -------------------------------------------------------------------------

    #[test]
    fn determinism_two_identical_runs_match() {
        // JS reference: both runs produce actorCount=21, totalFractures=86
        let settings = SolverSettings {
            max_solver_iterations_per_frame: 24,
            compression_elastic_limit: 5.0,
            compression_fatal_limit: 10.0,
            tension_elastic_limit: 5.0,
            tension_fatal_limit: 10.0,
            shear_elastic_limit: 5.0,
            shear_fatal_limit: 10.0,
            ..SolverSettings::default()
        };

        let run = || {
            let wall = build_wall_scenario(&WallOptions::default());
            let (nodes, bonds) = wall.to_solver_descs();
            let mut solver = ExtStressSolver::new(&nodes, &bonds, &settings).unwrap();
            simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 30)
        };

        let (f1, a1) = run();
        let (f2, a2) = run();

        assert_eq!(f1, f2, "fracture count must be deterministic");
        assert_eq!(a1, a2, "actor count must be deterministic");
        // JS reference values
        assert_eq!(a1, 21, "JS: 21 actors");
        assert_eq!(f1, 86, "JS: 86 fractures");
    }

    #[test]
    fn determinism_parity_with_js() {
        // Run the exact same determinism test as the JS reference
        let settings = SolverSettings {
            max_solver_iterations_per_frame: 24,
            compression_elastic_limit: 5.0,
            compression_fatal_limit: 10.0,
            tension_elastic_limit: 5.0,
            tension_fatal_limit: 10.0,
            shear_elastic_limit: 5.0,
            shear_fatal_limit: 10.0,
            ..SolverSettings::default()
        };

        let wall = build_wall_scenario(&WallOptions::default());
        let (nodes, bonds) = wall.to_solver_descs();
        let mut solver = ExtStressSolver::new(&nodes, &bonds, &settings).unwrap();
        let (fractures, actors) = simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 30);

        // Must match JS exactly — both use same C++ solver
        assert_eq!(fractures, 86, "JS reference: 86 fractures");
        assert_eq!(actors, 21, "JS reference: 21 actors");
    }

    // -------------------------------------------------------------------------
    // Material strength comparison parity
    // -------------------------------------------------------------------------

    #[test]
    fn stronger_material_fewer_fractures() {
        // JS reference: weak(1e4) → 70 fractures, strong(1e6) → 67 fractures
        let run_with_scale = |scale: f32| {
            let wall = build_wall_scenario(&WallOptions::default());
            let (nodes, bonds) = wall.to_solver_descs();
            let settings = SolverSettings {
                max_solver_iterations_per_frame: 24,
                compression_elastic_limit: 0.0009 * scale,
                compression_fatal_limit: 0.0027 * scale,
                tension_elastic_limit: 0.0009 * scale,
                tension_fatal_limit: 0.0027 * scale,
                shear_elastic_limit: 0.0012 * scale,
                shear_fatal_limit: 0.0036 * scale,
                ..SolverSettings::default()
            };
            let mut solver = ExtStressSolver::new(&nodes, &bonds, &settings).unwrap();
            simulate(&mut solver, Vec3::new(0.0, -9.81, 0.0), 30)
        };

        let (weak_f, _) = run_with_scale(1e4);
        let (strong_f, _) = run_with_scale(1e6);

        assert!(
            strong_f <= weak_f,
            "stronger material should have <= fractures: strong={strong_f}, weak={weak_f}"
        );
    }

    // -------------------------------------------------------------------------
    // Column gravity fixture parity
    // -------------------------------------------------------------------------

    #[test]
    fn column_gravity_down_parity() {
        // JS reference: overstressed=1, converged=true, linearError=0, angularError=0
        let nodes = vec![
            NodeDesc {
                centroid: Vec3::new(0.0, 0.0, 0.0),
                mass: 0.0,
                volume: 1.0,
            },
            NodeDesc {
                centroid: Vec3::new(0.0, 1.0, 0.0),
                mass: 1.0,
                volume: 1.0,
            },
        ];
        let bonds = vec![BondDesc {
            centroid: Vec3::new(0.0, 0.5, 0.0),
            normal: Vec3::new(0.0, 1.0, 0.0),
            area: 1.0,
            node0: 0,
            node1: 1,
        }];
        let settings = SolverSettings {
            max_solver_iterations_per_frame: 16,
            compression_elastic_limit: 0.5,
            compression_fatal_limit: 1.0,
            tension_elastic_limit: 0.5,
            tension_fatal_limit: 1.0,
            shear_elastic_limit: 1e6,
            shear_fatal_limit: 1e6,
            ..SolverSettings::default()
        };

        let mut solver = ExtStressSolver::new(&nodes, &bonds, &settings).unwrap();
        solver.add_gravity(Vec3::new(0.0, -5.0, 0.0));
        solver.update();

        assert_eq!(solver.overstressed_bond_count(), 1, "JS: overstressed=1");
        assert!(solver.converged(), "JS: converged=true");
        assert!(solver.linear_error() < 1e-4, "JS: linearError≈0");
        assert!(solver.angular_error() < 1e-4, "JS: angularError≈0");
    }
}
