use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::{
    fs,
    path::{Path, PathBuf},
};

use chrono::Utc;
use runmat_accelerate_api::{
    AccelDownloadFuture, AccelProvider, ApiDeviceInfo, GpuTensorHandle, HostTensorOwned,
    HostTensorView,
};
use runmat_analysis_core::{
    AnalysisField, AnalysisFieldValues, AnalysisModel, AnalysisModelId, AnalysisStep,
    AnalysisStepKind, BoundaryCondition, BoundaryConditionKind, CfdSolveFamily,
    ConductivityFrequencyPoint, ElectromagneticDomain, EvidenceConfidence, LoadCase, LoadKind,
    MaterialAssignment, MaterialElectricalModel, MaterialMechanicalModel, MaterialModel,
    MaterialThermalModel, ReferenceFrame, StructuralElementKind,
};
use runmat_analysis_fea::{
    fea_acoustic_frequency_response_field_id, fea_cht_energy_residual_field_id,
    fea_cht_fluid_temperature_field_id, fea_cht_interface_heat_flux_field_id,
    fea_cht_interface_temperature_jump_field_id, fea_cht_solid_temperature_field_id,
    fea_electro_thermal_temperature_field_id, fea_electro_thermal_thermal_residual_field_id,
    fea_fsi_coupling_iteration_count_field_id, fea_fsi_fluid_pressure_field_id,
    fea_fsi_fluid_velocity_field_id, fea_fsi_interface_displacement_field_id,
    fea_fsi_interface_pressure_field_id, fea_fsi_interface_residual_field_id,
    fea_fsi_interface_traction_field_id, fea_fsi_structural_displacement_field_id,
    fea_modal_mode_shape_field_id, fea_nonlinear_contact_gap_field_id,
    fea_nonlinear_contact_pressure_field_id, fea_nonlinear_equivalent_plastic_strain_field_id,
    fea_nonlinear_load_factor_field_id, fea_nonlinear_plastic_strain_field_id,
    fea_nonlinear_residual_norm_field_id, fea_nonlinear_rotation_field_id,
    fea_nonlinear_von_mises_field_id, fea_thermal_boundary_heat_flux_field_id,
    fea_thermal_heat_flux_field_id, fea_thermal_heat_source_field_id,
    fea_thermal_temperature_gradient_field_id, fea_thermo_mechanical_coupling_residual_field_id,
    fea_thermo_mechanical_displacement_field_id, fea_thermo_mechanical_temperature_field_id,
    fea_thermo_mechanical_thermal_strain_field_id, fea_thermo_mechanical_thermal_stress_field_id,
    fea_thermo_mechanical_von_mises_field_id, fea_transient_acceleration_field_id,
    fea_transient_angular_acceleration_field_id, fea_transient_angular_velocity_field_id,
    fea_transient_kinetic_energy_field_id, fea_transient_residual_norm_field_id,
    fea_transient_rotation_field_id, fea_transient_strain_energy_field_id,
    fea_transient_velocity_field_id, fea_transient_von_mises_field_id, ComputeBackend,
    FeaProgressPhase, FeaProgressStatus, FEA_FIELD_ACOUSTIC_PARTICLE_VELOCITY,
    FEA_FIELD_ACOUSTIC_PRESSURE_MAGNITUDE, FEA_FIELD_ACOUSTIC_PRESSURE_REAL,
    FEA_FIELD_CFD_PRESSURE, FEA_FIELD_CFD_RESIDUAL_CONTINUITY, FEA_FIELD_CFD_RESIDUAL_MOMENTUM,
    FEA_FIELD_CFD_REYNOLDS_NUMBER, FEA_FIELD_CFD_VELOCITY, FEA_FIELD_CFD_VORTICITY,
    FEA_FIELD_CFD_WALL_SHEAR_STRESS, FEA_FIELD_CHT_FLUID_PRESSURE, FEA_FIELD_CHT_FLUID_VELOCITY,
    FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY, FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD,
    FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL, FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT,
    FEA_FIELD_EM_CURRENT_DENSITY_REAL, FEA_FIELD_EM_ELECTRIC_FIELD_REAL,
    FEA_FIELD_EM_ELECTRIC_FLUX_DENSITY_REAL, FEA_FIELD_EM_ENERGY_DENSITY,
    FEA_FIELD_EM_MAGNETIC_FIELD_REAL, FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_MAGNITUDE,
    FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_REAL, FEA_FIELD_EM_POYNTING_VECTOR_REAL,
    FEA_FIELD_EM_RESIDUAL_REAL, FEA_FIELD_EM_VECTOR_POTENTIAL_IMAG,
    FEA_FIELD_EM_VECTOR_POTENTIAL_REAL, FEA_FIELD_MODAL_EIGENVALUE, FEA_FIELD_MODAL_FREQUENCY_HZ,
    FEA_FIELD_MODAL_MODAL_MASS, FEA_FIELD_MODAL_MODAL_STIFFNESS, FEA_FIELD_MODAL_M_ORTHOGONALITY,
    FEA_FIELD_MODAL_PARTICIPATION_FACTOR, FEA_FIELD_MODAL_RELATIVE_FREQUENCY_SEPARATION,
    FEA_FIELD_MODAL_RESIDUAL_NORM, FEA_FIELD_STRUCTURAL_BEAM_AXIAL_FORCE,
    FEA_FIELD_STRUCTURAL_BEAM_BENDING_MOMENT, FEA_FIELD_STRUCTURAL_BEAM_BENDING_STRESS,
    FEA_FIELD_STRUCTURAL_BEAM_SHEAR_FORCE, FEA_FIELD_STRUCTURAL_BEAM_TORSION_MOMENT,
    FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS, FEA_FIELD_STRUCTURAL_DISPLACEMENT,
    FEA_FIELD_STRUCTURAL_EQUATION_SCALE, FEA_FIELD_STRUCTURAL_REACTION_FORCE,
    FEA_FIELD_STRUCTURAL_REACTION_MOMENT, FEA_FIELD_STRUCTURAL_RESIDUAL_NORM,
    FEA_FIELD_STRUCTURAL_ROTATION, FEA_FIELD_STRUCTURAL_SHELL_BENDING_MOMENT,
    FEA_FIELD_STRUCTURAL_SHELL_MEMBRANE_FORCE, FEA_FIELD_STRUCTURAL_SHELL_TRANSVERSE_SHEAR,
    FEA_FIELD_STRUCTURAL_SHELL_VON_MISES, FEA_FIELD_STRUCTURAL_STRAIN, FEA_FIELD_STRUCTURAL_STRESS,
    FEA_FIELD_STRUCTURAL_TOTAL_STRAIN_ENERGY, FEA_FIELD_STRUCTURAL_VON_MISES,
};
use runmat_geometry_core::{
    GeometryAsset, GeometrySource, MaterialEvidence, MaterialEvidenceConfidence, MeshDescriptor,
    MeshKind, Region, RegionEntityMapping, SourceGeometry, SourceGeometryKind, SurfaceMesh,
    TessellationProfile, UnitSystem,
};

use super::*;

const TRIANGLE_STL: &str = "solid tri\n  facet normal 0 0 1\n    outer loop\n      vertex 0 0 0\n      vertex 1 0 0\n      vertex 0 1 0\n    endloop\n  endfacet\nendsolid tri\n";
const SIMPLE_STEP: &str = "ISO-10303-21;\nHEADER;\nFILE_NAME('Assembly_A');\nENDSEC;\nDATA;\n#10=PRODUCT('Bracket_A','',(#1));\nENDSEC;\nEND-ISO-10303-21;\n";

fn analysis_test_guard() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn sample_analysis_run_prep_context() -> AnalysisRunPrepContext {
    AnalysisRunPrepContext {
        prepared_mesh_count: 1,
        prepared_node_count: 16,
        prepared_element_count: 20,
        mapped_region_count: 3,
        min_scaled_jacobian: 0.86,
        mean_aspect_ratio: 1.5,
        inverted_element_count: 0,
        mapped_load_count: 1,
        mapped_bc_count: 3,
        layout_seed: 29,
        topology_dof_multiplier: 1.2,
        topology_bandwidth_estimate: 4,
        mapped_region_participation_ratio: 0.9,
        topology_surface_patch_ratio: 0.35,
        topology_volume_core_ratio: 0.55,
        topology_mixed_family_ratio: 0.05,
        topology_region_span_mean: 5.0,
        topology_region_block_count: 3,
        topology_region_mesh_mean: 4.0,
        topology_region_mesh_variance: 0.5,
        topology_triangle_family_ratio: 0.2,
        topology_quad_family_ratio: 0.3,
        topology_tet_family_ratio: 0.25,
        topology_hex_family_ratio: 0.25,
        coordinate_span_x_m: 2.4,
        coordinate_span_y_m: 0.6,
        coordinate_span_z_m: 0.4,
        coordinate_active_dimension_count: 3,
        coordinate_characteristic_length_m: 0.2,
        element_geometry_node_count: 4,
        element_geometry_edge_count: 5,
        mean_element_edge_length_m: 0.2,
        mean_element_area_m2: 0.04,
        element_geometry_coverage_ratio: 1.0,
        reference_element_coordinates_m: [[0.0, 0.0, 0.0], [0.4, 0.0, 0.0], [0.0, 0.2, 0.0]],
        reference_element_area_m2: 0.04,
        control_volume_cell_count: 15,
        control_volume_face_count: 19,
        control_volume_internal_face_count: 11,
        control_volume_boundary_face_count: 8,
        control_volume_connectivity_coverage_ratio: 1.0,
        element_topology_sample_element_count: 2,
        element_topology_sample_edge_count: 5,
        element_topology_sample_edge_nodes: [
            [0, 1],
            [1, 2],
            [0, 2],
            [2, 3],
            [0, 3],
            [0, 0],
            [0, 0],
            [0, 0],
        ],
        element_topology_sample_node_coordinates_m: [
            [0.0, 0.0, 0.0],
            [0.4, 0.0, 0.0],
            [0.0, 0.2, 0.0],
            [0.4, 0.2, 0.0],
            [0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0],
        ],
        element_topology_sample_element_edges: [[0, 1, 2], [2, 3, 4], [0, 0, 0], [0, 0, 0]],
        element_topology_sample_element_orientations: [
            [1, 1, -1],
            [1, 1, -1],
            [0, 0, 0],
            [0, 0, 0],
        ],
        element_topology_sample_element_areas_m2: [0.04, 0.04, 0.0, 0.0],
        element_topology_node_coordinates_m: vec![
            [0.0, 0.0, 0.0],
            [0.4, 0.0, 0.0],
            [0.0, 0.2, 0.0],
            [0.4, 0.2, 0.0],
        ],
        element_topology_edge_nodes: vec![[0, 1], [1, 2], [0, 2], [2, 3], [0, 3]],
        element_topology_element_edges: vec![[0, 1, 2], [2, 3, 4]],
        element_topology_element_orientations: vec![[1, 1, -1], [1, 1, -1]],
        element_topology_element_areas_m2: vec![0.04, 0.04],
    }
}

fn sample_model() -> AnalysisModel {
    AnalysisModel {
        model_id: AnalysisModelId("beam_model".to_string()),
        geometry_id: "geo:beam".to_string(),
        geometry_revision: 1,
        units: UnitSystem::Meter,
        frame: ReferenceFrame::Global,
        materials: vec![MaterialModel {
            material_id: "mat_steel".to_string(),
            name: "Steel".to_string(),
            mechanical: MaterialMechanicalModel {
                youngs_modulus_pa: 200e9,
                poisson_ratio: 0.3,
                density_kg_per_m3: 7850.0,
            },
            thermal: MaterialThermalModel {
                reference_temperature_k: 293.15,
                modulus_temp_coeff_per_k: -2.5e-4,
                ..MaterialThermalModel::default()
            },
            acoustic: None,
            electrical: None,
            plastic: None,
        }],
        material_assignments: Vec::new(),
        structural: None,
        thermo_mechanical: None,
        electro_thermal: None,
        electromagnetic: None,
        cfd: None,
        interfaces: Vec::new(),
        boundary_conditions: vec![BoundaryCondition {
            bc_id: "bc_root".to_string(),
            region_id: "root".to_string(),
            kind: BoundaryConditionKind::Fixed,
        }],
        loads: vec![LoadCase {
            load_id: "load_tip".to_string(),
            region_id: "tip".to_string(),
            kind: LoadKind::Force {
                fx: 0.0,
                fy: -1000.0,
                fz: 0.0,
            },
        }],
        steps: vec![AnalysisStep {
            step_id: "step_static".to_string(),
            kind: AnalysisStepKind::Static,
        }],
    }
}

fn sample_model_with_material_assignment_mismatch() -> AnalysisModel {
    let mut model = sample_model();
    model.materials.push(MaterialModel {
        material_id: "mat_polymer".to_string(),
        name: "Polymer".to_string(),
        mechanical: MaterialMechanicalModel {
            youngs_modulus_pa: 3.2e9,
            poisson_ratio: 0.37,
            density_kg_per_m3: 1150.0,
        },
        thermal: MaterialThermalModel {
            reference_temperature_k: 293.15,
            modulus_temp_coeff_per_k: -7.0e-4,
            ..MaterialThermalModel::default()
        },
        acoustic: None,
        electrical: None,
        plastic: None,
    });
    model.material_assignments = vec![MaterialAssignment {
        region_id: "tip".to_string(),
        expected_material_id: "mat_steel".to_string(),
        assigned_material_id: "mat_polymer".to_string(),
        confidence: EvidenceConfidence::Verified,
    }];
    model
}

fn sample_cfd_domain(
    solve_family: CfdSolveFamily,
    enabled: bool,
) -> runmat_analysis_core::CfdDomain {
    runmat_analysis_core::CfdDomain {
        enabled,
        solve_family,
        reference_density_kg_per_m3: 1.225,
        dynamic_viscosity_pa_s: 1.81e-5,
        inlet_velocity_m_per_s: 5.0,
        turbulence_intensity: 0.06,
        time_profile: Vec::new(),
    }
}

fn sample_moment_load(load_id: &str) -> LoadCase {
    LoadCase {
        load_id: load_id.to_string(),
        region_id: "tip".to_string(),
        kind: LoadKind::Moment {
            mx: 1.0,
            my: 2.0,
            mz: 3.0,
        },
    }
}

fn sample_cfd_boundary_conditions(inlet_velocity_m_per_s: f64) -> Vec<BoundaryCondition> {
    vec![
        BoundaryCondition {
            bc_id: "bc_cfd_inlet".to_string(),
            region_id: "fluid_inlet".to_string(),
            kind: BoundaryConditionKind::CfdInletVelocity {
                velocity_m_per_s: inlet_velocity_m_per_s,
            },
        },
        BoundaryCondition {
            bc_id: "bc_cfd_outlet".to_string(),
            region_id: "fluid_outlet".to_string(),
            kind: BoundaryConditionKind::CfdOutletPressure { pressure_pa: 0.0 },
        },
        BoundaryCondition {
            bc_id: "bc_cfd_wall_upper".to_string(),
            region_id: "fluid_wall_upper".to_string(),
            kind: BoundaryConditionKind::CfdNoSlipWall,
        },
        BoundaryCondition {
            bc_id: "bc_cfd_wall_lower".to_string(),
            region_id: "fluid_wall_lower".to_string(),
            kind: BoundaryConditionKind::CfdNoSlipWall,
        },
    ]
}

fn sample_cht_model() -> AnalysisModel {
    let mut model = sample_model();
    model.steps = vec![
        AnalysisStep {
            step_id: "cht_flow".to_string(),
            kind: AnalysisStepKind::Cfd,
        },
        AnalysisStep {
            step_id: "cht_thermal".to_string(),
            kind: AnalysisStepKind::Thermal,
        },
    ];
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::Transient, true));
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 60.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: vec![ThermoRegionTemperatureDelta {
                region_id: "tip".to_string(),
                temperature_delta_k: 70.0,
            }],
            time_profile: vec![
                ThermoTimeProfilePoint {
                    normalized_time: 0.0,
                    scale: 0.5,
                },
                ThermoTimeProfilePoint {
                    normalized_time: 1.0,
                    scale: 1.0,
                },
            ],
        },
    );
    model
}

fn sample_fsi_model() -> AnalysisModel {
    let mut model = sample_model();
    model.steps = vec![
        AnalysisStep {
            step_id: "fsi_structure".to_string(),
            kind: AnalysisStepKind::Transient,
        },
        AnalysisStep {
            step_id: "fsi_flow".to_string(),
            kind: AnalysisStepKind::Cfd,
        },
    ];
    model.cfd = Some(runmat_analysis_core::CfdDomain {
        enabled: true,
        solve_family: CfdSolveFamily::Transient,
        reference_density_kg_per_m3: 1.225,
        dynamic_viscosity_pa_s: 1.81e-5,
        inlet_velocity_m_per_s: 4.0,
        turbulence_intensity: 0.06,
        time_profile: vec![
            runmat_analysis_core::CfdTimeProfilePoint {
                normalized_time: 0.0,
                inlet_scale: 0.6,
            },
            runmat_analysis_core::CfdTimeProfilePoint {
                normalized_time: 1.0,
                inlet_scale: 1.0,
            },
        ],
    });
    model
}

fn set_model_thermo_coupling(model: &mut AnalysisModel, coupling: ThermoMechanicalCouplingOptions) {
    model.thermo_mechanical = Some(runmat_analysis_core::ThermoMechanicalDomain {
        enabled: coupling.enabled,
        reference_temperature_k: coupling.reference_temperature_k,
        applied_temperature_delta_k: coupling.applied_temperature_delta_k,
        field_artifact_id: coupling.field_artifact_id,
        field_source: coupling
            .field_source
            .map(|source| runmat_analysis_core::ThermoFieldSource {
                source_id: source.source_id,
                revision: source.revision,
                interpolation_mode: source.interpolation_mode.map(|mode| match mode {
                    ThermoFieldInterpolationMode::Linear => {
                        runmat_analysis_core::ThermoFieldInterpolationMode::Linear
                    }
                    ThermoFieldInterpolationMode::Step => {
                        runmat_analysis_core::ThermoFieldInterpolationMode::Step
                    }
                }),
                expected_region_ids: source.expected_region_ids,
            }),
        region_temperature_deltas: coupling
            .region_temperature_deltas
            .into_iter()
            .map(|delta| runmat_analysis_core::ThermoRegionTemperatureDelta {
                region_id: delta.region_id,
                temperature_delta_k: delta.temperature_delta_k,
            })
            .collect(),
        time_profile: coupling
            .time_profile
            .into_iter()
            .map(|point| runmat_analysis_core::ThermoTimeProfilePoint {
                normalized_time: point.normalized_time,
                scale: point.scale,
            })
            .collect(),
    });
}

fn set_model_electro_coupling(model: &mut AnalysisModel, coupling: ElectroThermalCouplingOptions) {
    for material in &mut model.materials {
        material.electrical = Some(runmat_analysis_core::MaterialElectricalModel {
            reference_temperature_k: coupling.reference_temperature_k,
            conductivity_s_per_m: coupling.base_electrical_conductivity_s_per_m,
            resistive_heating_coefficient: coupling.resistive_heating_coefficient,
            relative_permittivity: 1.0,
            relative_permeability: 1.0,
            conductivity_frequency_response: Vec::new(),
        });
    }
    model.electro_thermal = Some(runmat_analysis_core::ElectroThermalDomain {
        enabled: coupling.enabled,
        reference_temperature_k: coupling.reference_temperature_k,
        applied_voltage_v: coupling.applied_voltage_v,
        region_conductivity_scales: coupling
            .region_conductivity_scales
            .into_iter()
            .map(
                |scale| runmat_analysis_core::ElectroRegionConductivityScale {
                    region_id: scale.region_id,
                    conductivity_scale: scale.conductivity_scale,
                },
            )
            .collect(),
        time_profile: coupling
            .time_profile
            .into_iter()
            .map(|point| runmat_analysis_core::ElectroTimeProfilePoint {
                normalized_time: point.normalized_time,
                current_scale: point.current_scale,
            })
            .collect(),
    });
}

fn set_model_plasticity(model: &mut AnalysisModel, plasticity: PlasticityConstitutiveOptions) {
    if !plasticity.enabled {
        return;
    }
    for material in &mut model.materials {
        material.plastic = Some(runmat_analysis_core::MaterialPlasticModel {
            yield_strain: plasticity.yield_strain,
            hardening_modulus_ratio: plasticity.hardening_modulus_ratio,
            saturation_exponent: plasticity.saturation_exponent,
        });
    }
}

fn set_model_contact(model: &mut AnalysisModel, contact: ContactInterfaceOptions) {
    if !contact.enabled {
        return;
    }
    model.interfaces = vec![runmat_analysis_core::AnalysisInterface {
        interface_id: "contact_1".to_string(),
        primary_region_id: "root".to_string(),
        secondary_region_id: "tip".to_string(),
        kind: runmat_analysis_core::AnalysisInterfaceKind::Contact(
            runmat_analysis_core::ContactInterfaceModel {
                penalty_stiffness_scale: contact.penalty_stiffness_scale,
                max_penetration_ratio: contact.max_penetration_ratio,
                friction_coefficient: contact.friction_coefficient,
            },
        ),
    }];
}

fn sample_geometry_asset() -> GeometryAsset {
    GeometryAsset {
        geometry_id: "geo:beam".to_string(),
        source: GeometrySource {
            path: "/fixtures/beam.stl".to_string(),
            sha256: "hash-beam".to_string(),
            importer_version: "stl/v1".to_string(),
        },
        source_geometry: SourceGeometry {
            kind: SourceGeometryKind::Mesh,
            assembly: None,
            material_evidence: Vec::new(),
        },
        tessellation_profile: TessellationProfile::default(),
        units: UnitSystem::Meter,
        revision: 2,
        meshes: vec![MeshDescriptor {
            mesh_id: "mesh_1".to_string(),
            kind: MeshKind::Surface,
            vertex_count: 3,
            element_count: 1,
        }],
        surface_meshes: vec![SurfaceMesh::new(
            "mesh_1",
            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            vec![[0, 1, 2]],
        )],
        regions: vec![Region {
            region_id: "region_default".to_string(),
            name: "Default Region".to_string(),
            tag: Some("mesh_default".to_string()),
            cad_ownership: None,
        }],
        region_entity_mappings: vec![RegionEntityMapping::all_faces(
            "region_default",
            "mesh_1",
            1,
        )],
        diagnostics: Vec::new(),
    }
}

fn sample_prep_artifact_id_for_model(model: &AnalysisModel) -> String {
    let mut geometry = sample_geometry_asset();
    geometry.geometry_id = model.geometry_id.clone();
    geometry.revision = model.geometry_revision;
    let prep = crate::geometry::geometry_prep_for_analysis_op(
        &geometry,
        crate::geometry::GeometryPrepForAnalysisSpec::default(),
        OperationContext::new(None, None),
    )
    .expect("prep should succeed");
    prep.data.prep_artifact_id
}

fn sample_step_like_geometry_asset() -> GeometryAsset {
    let mut asset = sample_geometry_asset();
    asset.source_geometry.kind = SourceGeometryKind::Cad;
    asset.source_geometry.material_evidence = vec![MaterialEvidence {
        source_key: "STEP:MATERIAL".to_string(),
        normalized_key: "material_name".to_string(),
        value: "Aluminum 6061".to_string(),
        confidence: MaterialEvidenceConfidence::High,
        unit_basis: None,
        assumptions: vec!["imported".to_string()],
    }];
    asset.regions = vec![
        Region {
            region_id: "region_root".to_string(),
            name: "Base_Mount".to_string(),
            tag: Some("fixed".to_string()),
            cad_ownership: None,
        },
        Region {
            region_id: "region_tip".to_string(),
            name: "Tip_Load".to_string(),
            tag: Some("load".to_string()),
            cad_ownership: None,
        },
    ];
    asset.region_entity_mappings = vec![
        RegionEntityMapping::all_faces("region_root", "mesh_1", 1),
        RegionEntityMapping::all_faces("region_tip", "mesh_1", 1),
    ];
    asset
}

fn sample_linear_static_study_spec() -> AnalysisStudySpec {
    AnalysisStudySpec {
        study_id: "study_linear_static_001".to_string(),
        geometry: sample_geometry_asset(),
        create_model_intent: AnalysisCreateModelIntentSpec {
            model_id: "study_model_linear_static_001".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: None,
        },
        model: None,
        run_kind: AnalysisRunKind::LinearStatic,
        backend: ComputeBackend::Cpu,
        linear_static_run_options: None,
        modal_run_options: None,
        acoustic_run_options: None,
        thermal_run_options: None,
        transient_run_options: None,
        cfd_run_options: None,
        cht_run_options: None,
        fsi_run_options: None,
        nonlinear_run_options: None,
        electromagnetic_run_options: None,
    }
}

fn sample_electromagnetic_study_spec() -> AnalysisStudySpec {
    AnalysisStudySpec {
        study_id: "study_electromagnetic_001".to_string(),
        geometry: sample_geometry_asset(),
        create_model_intent: AnalysisCreateModelIntentSpec {
            model_id: "study_model_electromagnetic_001".to_string(),
            profile: AnalysisCreateModelProfile::ElectromagneticStatic,
            prep_context: None,
        },
        model: None,
        run_kind: AnalysisRunKind::Electromagnetic,
        backend: ComputeBackend::Cpu,
        linear_static_run_options: None,
        modal_run_options: None,
        acoustic_run_options: None,
        thermal_run_options: None,
        transient_run_options: None,
        cfd_run_options: None,
        cht_run_options: None,
        fsi_run_options: None,
        nonlinear_run_options: None,
        electromagnetic_run_options: None,
    }
}

#[test]
fn fea_document_resolves_study_geometry_and_run_options() {
    let _guard = analysis_test_guard();
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    fs::write(tmp.path().join("part.stl"), TRIANGLE_STL).expect("fixture geometry should write");
    let input = r#"
version: 1
kind: study
id: bracket_static
geometry:
  path: part.stl
  units: meter
model:
  profile: linear_static_structural
run:
  backend: cpu
  options:
    deterministic_mode: true
    precision_mode: fp64
    preconditioner_mode: jacobi
    quality_policy: strict
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA study document should resolve");

    let FeaResolvedDocument::Study(spec) = resolved else {
        panic!("expected resolved study");
    };
    assert_eq!(spec.study_id, "bracket_static");
    assert_eq!(spec.geometry.units, UnitSystem::Meter);
    assert!(spec.geometry.source.path.ends_with("part.stl"));
    assert_eq!(
        spec.create_model_intent.profile,
        AnalysisCreateModelProfile::LinearStaticStructural
    );
    assert_eq!(spec.create_model_intent.model_id, "bracket_static_model");
    assert_eq!(spec.run_kind, AnalysisRunKind::LinearStatic);
    assert_eq!(spec.backend, ComputeBackend::Cpu);
    assert!(spec.model.is_none());

    let options = spec
        .linear_static_run_options
        .expect("linear static options should parse");
    assert!(options.deterministic_mode);
    assert_eq!(options.precision_mode, PrecisionMode::Fp64);
    assert_eq!(options.preconditioner_mode, PreconditionerMode::Jacobi);
    assert_eq!(options.quality_policy, QualityPolicy::Strict);
}

#[test]
fn fea_document_rejects_legacy_run_kind_profile_mismatch() {
    let _guard = analysis_test_guard();
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    fs::write(tmp.path().join("part.stl"), TRIANGLE_STL).expect("fixture geometry should write");
    let input = r#"
version: 1
kind: study
id: bracket_thermal
geometry:
  path: part.stl
  units: meter
model:
  profile: thermal_standalone
run:
  kind: linear_static
  backend: cpu
"#;

    let err = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect_err("mismatched legacy run.kind should fail");

    assert!(err.contains("run.kind"));
    assert!(err.contains("model.profile"));
}

#[test]
fn fea_document_resolves_explicit_model_and_sweep() {
    let _guard = analysis_test_guard();
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    fs::write(tmp.path().join("assembly.step"), SIMPLE_STEP)
        .expect("fixture geometry should write");
    let input = r#"
version: 1
kind: sweep
id: bracket_sweep
fail_fast: false
studies:
  - version: 1
    id: bracket_static_a
    geometry:
      path: assembly.step
      units: millimeter
    model:
      id: bracket_model
      profile: linear_static_structural
      defaults: none
      frame: global
    regions:
      bracket:
        selector: "name:Bracket_A"
    materials:
      aluminum:
        name: Aluminum 6061
        mechanical:
          youngs_modulus_pa: 69000000000.0
          poisson_ratio: 0.33
    material_assignments:
      - region: bracket
        material: aluminum
    boundary_conditions:
      - id: fixed_bracket
        region: bracket
        kind: fixed
    loads:
      - id: pressure_load
        region: bracket
        type: pressure
        magnitude_pa: 1200.0
    steps:
      - id: static_step
        kind: static
    run:
      backend: cpu
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA sweep document should resolve");

    let FeaResolvedDocument::Sweep(sweep) = resolved else {
        panic!("expected resolved sweep");
    };
    assert_eq!(sweep.sweep_id, "bracket_sweep");
    assert!(!sweep.fail_fast);
    assert_eq!(sweep.studies.len(), 1);

    let study = &sweep.studies[0];
    assert_eq!(study.study_id, "bracket_static_a");
    assert_eq!(study.create_model_intent.model_id, "bracket_model");
    assert_eq!(study.geometry.units, UnitSystem::Millimeter);
    let model = study.model.as_ref().expect("explicit model should resolve");
    assert_eq!(model.model_id.0, "bracket_model");
    assert_eq!(model.materials.len(), 1);
    assert_eq!(model.material_assignments.len(), 1);
    assert_eq!(model.material_assignments[0].region_id, "region_1");
    assert_eq!(model.boundary_conditions.len(), 1);
    assert_eq!(model.loads.len(), 1);
    assert_eq!(model.steps.len(), 1);
}

#[test]
fn fea_document_resolves_moment_loads() {
    let _guard = analysis_test_guard();
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    fs::write(tmp.path().join("assembly.step"), SIMPLE_STEP)
        .expect("fixture geometry should write");
    let input = r#"
version: 1
kind: study
id: beam_static
geometry:
  path: assembly.step
  units: meter
model:
  id: beam_model
  profile: linear_static_structural
  defaults: none
regions:
  tip:
    selector: "name:Bracket_A"
materials:
  steel:
    mechanical:
      youngs_modulus_pa: 200000000000.0
      poisson_ratio: 0.30
material_assignments:
  - region: tip
    material: steel
boundary_conditions:
  - id: fixed_tip
    region: tip
    kind: fixed
loads:
  - id: tip_moment
    region: tip
    type: moment
    vector: [10.0, 20.0, 30.0]
steps:
  - id: static_step
    kind: static
run:
  backend: cpu
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA study document should resolve");

    let FeaResolvedDocument::Study(study) = resolved else {
        panic!("expected resolved study");
    };
    let model = study.model.as_ref().expect("explicit model should resolve");
    assert_eq!(model.loads.len(), 1);
    assert!(matches!(
        model.loads[0].kind,
        LoadKind::Moment {
            mx: 10.0,
            my: 20.0,
            mz: 30.0
        }
    ));
}

#[test]
fn fea_document_resolves_prescribed_rotation_boundary_conditions() {
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    let geometry_path = tmp.path().join("part.step");
    std::fs::write(&geometry_path, SIMPLE_STEP).expect("geometry fixture should write");

    let input = r#"
version: 1
kind: study
id: rotation_bc_static
geometry:
  path: part.step
  units: meter
model:
  id: rotation_bc_model
  profile: linear_static_structural
  defaults: none
regions:
  tip:
    selector: "name:Bracket_A"
materials:
  steel:
    mechanical:
      youngs_modulus_pa: 200000000000.0
      poisson_ratio: 0.30
material_assignments:
  - region: tip
    material: steel
boundary_conditions:
  - id: tip_rotation
    region: tip
    type: prescribed_rotation
    rx: 0.1
    ry: 0.2
    rz: 0.3
loads:
  - id: tip_force
    region: tip
    type: force
    vector: [0.0, -1000.0, 0.0]
steps:
  - id: static_step
    kind: static
run:
  backend: cpu
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA study document should resolve");

    let FeaResolvedDocument::Study(study) = resolved else {
        panic!("expected resolved study");
    };
    let model = study.model.as_ref().expect("explicit model should resolve");
    assert_eq!(model.boundary_conditions.len(), 1);
    assert!(matches!(
        model.boundary_conditions[0].kind,
        BoundaryConditionKind::PrescribedRotation {
            rx: 0.1,
            ry: 0.2,
            rz: 0.3
        }
    ));
}

#[test]
fn fea_document_resolves_beam_structural_model_data() {
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    std::fs::write(tmp.path().join("assembly.step"), SIMPLE_STEP)
        .expect("fixture geometry should write");
    let input = r#"
version: 1
kind: study
id: beam_static
geometry:
  path: assembly.step
  units: meter
model:
  id: beam_model
  profile: linear_static_structural
  defaults: none
regions:
  beam_span:
    selector: "name:Bracket_A"
materials:
  steel:
    mechanical:
      youngs_modulus_pa: 200000000000.0
      poisson_ratio: 0.30
material_assignments:
  - region: beam_span
    material: steel
nodes:
  - id: 1
    coordinates_m: [0.0, 0.0, 0.0]
  - id: 2
    coordinates_m: [1.0, 0.0, 0.0]
elements:
  - id: beam_1
    region: beam_span
    type: beam
    nodes: [1, 2]
    section: rect_20x10
    reference_axis: [0.0, 0.0, 1.0]
sections:
  - id: rect_20x10
    area_m2: 2.0e-4
    iy_m4: 1.6e-9
    iz_m4: 6.4e-9
    torsion_j_m4: 2.4e-9
    outer_fiber_y_m: 0.01
    outer_fiber_z_m: 0.005
    torsion_outer_radius_m: 0.011180339887498949
boundary_conditions:
  - id: fixed_root
    region: beam_span
    kind: fixed
loads:
  - id: tip_force
    region: beam_span
    type: force
    vector: [0.0, -1000.0, 0.0]
steps:
  - id: static_step
    kind: static
run:
  backend: cpu
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA study document should resolve");

    let FeaResolvedDocument::Study(study) = resolved else {
        panic!("expected resolved study");
    };
    let model = study.model.as_ref().expect("explicit model should resolve");
    let structural = model
        .structural
        .as_ref()
        .expect("structural model should resolve");
    assert_eq!(structural.nodes.len(), 2);
    assert_eq!(structural.elements.len(), 1);
    assert_eq!(structural.beam_sections.len(), 1);
    assert_eq!(structural.elements[0].region_id, "region_1");
    let beam = match &structural.elements[0].kind {
        StructuralElementKind::Beam(beam) => beam,
        StructuralElementKind::Shell(_) => panic!("expected beam element"),
    };
    assert_eq!(beam.node_ids, [1, 2]);
    assert_eq!(beam.section_id, "rect_20x10");
    assert_eq!(beam.reference_axis, [0.0, 0.0, 1.0]);
    assert_eq!(structural.beam_sections[0].outer_fiber_y_m, 0.01);
    assert_eq!(structural.beam_sections[0].outer_fiber_z_m, 0.005);
    assert_eq!(
        structural.beam_sections[0].torsion_outer_radius_m,
        0.011_180_339_887_498_949
    );
}

#[test]
fn fea_document_resolves_shell_structural_model_data() {
    let tmp = tempfile::tempdir().expect("tempdir should be created");
    std::fs::write(tmp.path().join("assembly.step"), SIMPLE_STEP)
        .expect("fixture geometry should write");
    let input = r#"
version: 1
kind: study
id: shell_static
geometry:
  path: assembly.step
  units: meter
model:
  id: shell_model
  profile: linear_static_structural
  defaults: none
regions:
  shell_panel:
    selector: "name:Bracket_A"
materials:
  steel:
    mechanical:
      youngs_modulus_pa: 200000000000.0
      poisson_ratio: 0.30
      density_kg_per_m3: 7850.0
material_assignments:
  - region: shell_panel
    material: steel
nodes:
  - id: 1
    coordinates_m: [0.0, 0.0, 0.0]
  - id: 2
    coordinates_m: [1.0, 0.0, 0.0]
  - id: 3
    coordinates_m: [0.0, 1.0, 0.0]
elements:
  - id: shell_1
    region: shell_panel
    type: shell
    nodes: [1, 2, 3]
    section: panel_2mm
    reference_axis: [1.0, 0.0, 0.0]
sections:
  - id: panel_2mm
    type: shell
    thickness_m: 0.002
    shear_correction: 0.8333333333333334
    drilling_stiffness_scale: 0.0001
boundary_conditions:
  - id: fixed_edge
    region: shell_panel
    kind: fixed
loads:
  - id: edge_moment
    region: shell_panel
    type: moment
    vector: [0.0, 5.0, 0.0]
steps:
  - id: static_step
    kind: static
run:
  backend: cpu
"#;

    let resolved = pollster::block_on(parse_and_resolve_fea_document(input, tmp.path()))
        .expect("FEA shell study document should resolve");

    let FeaResolvedDocument::Study(study) = resolved else {
        panic!("expected resolved study");
    };
    let structural = study
        .model
        .as_ref()
        .and_then(|model| model.structural.as_ref())
        .expect("structural model should resolve");
    assert_eq!(structural.nodes.len(), 3);
    assert_eq!(structural.elements.len(), 1);
    assert_eq!(structural.beam_sections.len(), 0);
    assert_eq!(structural.shell_sections.len(), 1);
    let shell = match &structural.elements[0].kind {
        StructuralElementKind::Shell(shell) => shell,
        StructuralElementKind::Beam(_) => panic!("expected shell element"),
    };
    assert_eq!(shell.node_ids, [1, 2, 3]);
    assert_eq!(shell.section_id, "panel_2mm");
    assert_eq!(shell.reference_axis, [1.0, 0.0, 0.0]);
    assert_eq!(structural.shell_sections[0].thickness_m, 0.002);
    assert_eq!(structural.shell_sections[0].shear_correction, 5.0 / 6.0);
    assert_eq!(
        structural.shell_sections[0].drilling_stiffness_scale,
        1.0e-4
    );
}

#[test]
fn analysis_create_model_returns_v1_envelope() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "model_from_geo".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: None,
        },
        OperationContext::new(Some("trace-create-1".to_string()), None),
    )
    .expect("create model should pass");

    assert_eq!(envelope.operation, "fea.create_model");
    assert_eq!(envelope.op_version, "fea.create_model/v1");
    assert_eq!(envelope.data.model_id.0, "model_from_geo");
    assert_eq!(envelope.data.geometry_id, "geo:beam");
    assert_eq!(envelope.data.geometry_revision, 2);
    assert_eq!(envelope.data.units, UnitSystem::Meter);
    assert_eq!(envelope.data.frame, ReferenceFrame::Global);
    assert!(!envelope.data.materials.is_empty());
    assert!(!envelope.data.boundary_conditions.is_empty());
    assert!(!envelope.data.loads.is_empty());
    assert!(!envelope.data.steps.is_empty());
    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Static);
}

#[test]
fn transient_run_option_presets_are_ordered_for_cost_vs_accuracy() {
    let coarse = AnalysisTransientRunOptions::coarse();
    let balanced = AnalysisTransientRunOptions::balanced();
    let production = AnalysisTransientRunOptions::production_recommended();
    let high_accuracy = AnalysisTransientRunOptions::high_accuracy();

    assert!(coarse.step_count < balanced.step_count);
    assert!(balanced.step_count < high_accuracy.step_count);

    assert!(coarse.tolerance > balanced.tolerance);
    assert!(balanced.tolerance > high_accuracy.tolerance);

    assert!(coarse.time_step_s > balanced.time_step_s);
    assert!(balanced.time_step_s > high_accuracy.time_step_s);

    assert_eq!(production.quality_policy, QualityPolicy::Balanced);
    assert!(production.deterministic_mode);
    assert_eq!(production.precision_mode, PrecisionMode::Fp64);
    assert_eq!(production.dt_bucket_rel_tolerance, 0.01);
}

#[test]
fn modal_run_option_presets_are_ordered_for_cost_vs_accuracy() {
    let coarse = AnalysisModalRunOptions::coarse();
    let balanced = AnalysisModalRunOptions::balanced();
    let high_accuracy = AnalysisModalRunOptions::high_accuracy();

    assert!(coarse.mode_count < balanced.mode_count);
    assert!(balanced.mode_count < high_accuracy.mode_count);
    assert!(coarse.residual_warn_threshold > balanced.residual_warn_threshold);
    assert!(balanced.residual_warn_threshold > high_accuracy.residual_warn_threshold);
}

#[test]
fn nonlinear_run_option_presets_are_ordered_for_cost_vs_accuracy() {
    let coarse = AnalysisNonlinearRunOptions::coarse();
    let balanced = AnalysisNonlinearRunOptions::balanced();
    let production = AnalysisNonlinearRunOptions::production_recommended();
    let high_accuracy = AnalysisNonlinearRunOptions::high_accuracy();

    assert!(coarse.increment_count < balanced.increment_count);
    assert!(balanced.increment_count <= production.increment_count);
    assert!(production.increment_count <= high_accuracy.increment_count);

    assert!(coarse.max_newton_iters < balanced.max_newton_iters);
    assert!(balanced.max_newton_iters <= production.max_newton_iters);
    assert!(production.max_newton_iters <= high_accuracy.max_newton_iters);

    assert!(coarse.tolerance > balanced.tolerance);
    assert!(balanced.tolerance >= production.tolerance);
    assert!(production.tolerance >= high_accuracy.tolerance);

    assert_eq!(production.quality_policy, QualityPolicy::Balanced);
    assert!(production.deterministic_mode);
    assert_eq!(production.precision_mode, PrecisionMode::Fp64);
    assert!(production.line_search);
    assert!(production.max_line_search_backtracks >= balanced.max_line_search_backtracks);
}

#[test]
fn analysis_create_model_maps_invalid_intent_error() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let err = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "   ".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect_err("create model should fail");

    assert_eq!(err.error_code, "RM.FEA.CREATE_MODEL.INVALID_INTENT");
    assert_eq!(err.operation, "fea.create_model");
    assert_eq!(err.op_version, "fea.create_model/v1");
}

#[test]
fn analysis_create_model_supports_nonlinear_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "nonlinear_model".to_string(),
            profile: AnalysisCreateModelProfile::NonlinearStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear profile should be supported");

    assert_eq!(envelope.data.model_id.0, "nonlinear_model");
    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Nonlinear);
    assert_eq!(
        envelope.data.loads[0].load_id,
        "load_default_nonlinear_force"
    );
}

#[test]
fn analysis_create_model_accepts_prep_context_and_validates_model() {
    let _guard = analysis_test_guard();
    let _prep_guard = crate::geometry::prep_artifact_test_guard();
    let geometry = sample_step_like_geometry_asset();
    let prep = crate::geometry::geometry_prep_for_analysis_op(
        &geometry,
        crate::geometry::GeometryPrepForAnalysisSpec::default(),
        OperationContext::new(None, None),
    )
    .expect("prep for analysis should succeed");

    let created = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "prep_model".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: Some(AnalysisCreateModelPrepContext {
                source_geometry_id: prep.data.prep.provenance.source_geometry_id.clone(),
                source_geometry_revision: prep.data.prep.provenance.source_geometry_revision,
                region_mappings: prep.data.prep.region_mappings.clone(),
            }),
        },
        OperationContext::new(None, None),
    )
    .expect("create model with prep context should succeed");

    analysis_validate(
        &created.data,
        geometry.units,
        &ReferenceFrame::Global,
        OperationContext::new(None, None),
    )
    .expect("prep-aware created model should validate");
    assert_eq!(created.data.boundary_conditions[0].region_id, "region_root");
    assert_eq!(created.data.loads[0].region_id, "region_tip");
    assert!(created
        .data
        .material_assignments
        .iter()
        .all(|assignment| assignment.confidence
            == runmat_analysis_core::EvidenceConfidence::Verified));
}

#[test]
fn analysis_create_model_rejects_mismatched_prep_context() {
    let _guard = analysis_test_guard();
    let geometry = sample_step_like_geometry_asset();
    let error = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "bad_prep_model".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: Some(AnalysisCreateModelPrepContext {
                source_geometry_id: "geo:other".to_string(),
                source_geometry_revision: geometry.revision,
                region_mappings: Vec::new(),
            }),
        },
        OperationContext::new(None, None),
    )
    .expect_err("mismatched prep context should fail");
    assert_eq!(error.error_code, "RM.FEA.CREATE_MODEL.PREP_MISMATCH");
}

#[test]
fn analysis_create_model_supports_transient_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "transient_model".to_string(),
            profile: AnalysisCreateModelProfile::TransientStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("transient profile should be supported");

    assert_eq!(envelope.data.model_id.0, "transient_model");
    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Transient);
    assert_eq!(
        envelope.data.loads[0].load_id,
        "load_default_transient_force"
    );
}

#[test]
fn analysis_create_model_supports_modal_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal profile should be supported");

    assert_eq!(envelope.data.model_id.0, "modal_model");
    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Modal);
    assert_eq!(envelope.data.loads[0].load_id, "load_default_modal_seed");
}

#[test]
fn analysis_create_model_supports_acoustic_harmonic_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_harmonic_model".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic harmonic profile should be supported");

    assert_eq!(envelope.data.model_id.0, "acoustic_harmonic_model");
    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Modal);
    assert_eq!(
        envelope.data.loads[0].load_id,
        "load_default_acoustic_harmonic_seed"
    );
}

#[test]
fn analysis_create_model_supports_electromagnetic_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "electromagnetic_profile_model".to_string(),
            profile: AnalysisCreateModelProfile::ElectromagneticStatic,
            prep_context: None,
        },
        OperationContext::new(Some("trace-create-em-profile".to_string()), None),
    )
    .expect("electromagnetic profile model creation should succeed");

    assert_eq!(
        envelope.data.steps[0].kind,
        AnalysisStepKind::Electromagnetic
    );
    let domain = envelope
        .data
        .electromagnetic
        .as_ref()
        .expect("electromagnetic domain should be populated");
    assert!(domain.enabled);
    assert_eq!(domain.reference_frequency_hz, 60.0);
    assert_eq!(domain.applied_current_a, 100.0);
}

#[test]
fn analysis_create_model_supports_cfd_steady_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "cfd_steady_model".to_string(),
            profile: AnalysisCreateModelProfile::CfdSteadyState,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cfd steady profile should be supported");

    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Cfd);
    let cfd = envelope
        .data
        .cfd
        .as_ref()
        .expect("cfd domain should be populated");
    assert_eq!(cfd.solve_family, CfdSolveFamily::SteadyState);
    assert!(cfd.time_profile.is_empty());
}

#[test]
fn analysis_create_model_supports_cfd_transient_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "cfd_transient_model".to_string(),
            profile: AnalysisCreateModelProfile::CfdTransient,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cfd transient profile should be supported");

    assert_eq!(envelope.data.steps[0].kind, AnalysisStepKind::Cfd);
    let cfd = envelope
        .data
        .cfd
        .as_ref()
        .expect("cfd domain should be populated");
    assert_eq!(cfd.solve_family, CfdSolveFamily::Transient);
    assert_eq!(cfd.time_profile.len(), 2);
    assert_eq!(cfd.time_profile[0].normalized_time, 0.0);
    assert_eq!(cfd.time_profile[1].normalized_time, 1.0);
}

#[test]
fn analysis_create_model_supports_cht_coupled_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "cht_coupled_model".to_string(),
            profile: AnalysisCreateModelProfile::ChtCoupled,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cht coupled profile should be supported");

    assert_eq!(envelope.data.steps.len(), 2);
    assert!(envelope
        .data
        .steps
        .iter()
        .any(|step| step.kind == AnalysisStepKind::Cfd));
    assert!(envelope
        .data
        .steps
        .iter()
        .any(|step| step.kind == AnalysisStepKind::Thermal));
    let cfd = envelope
        .data
        .cfd
        .as_ref()
        .expect("cfd domain should be populated for cht profile");
    assert_eq!(cfd.solve_family, CfdSolveFamily::Transient);
    assert_eq!(cfd.time_profile.len(), 2);
    assert!(
        envelope
            .data
            .thermo_mechanical
            .as_ref()
            .expect("thermo-mechanical domain should be populated")
            .enabled
    );
}

#[test]
fn analysis_create_model_supports_fsi_coupled_profile_template() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "fsi_coupled_model".to_string(),
            profile: AnalysisCreateModelProfile::FsiCoupled,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("fsi coupled profile should be supported");

    assert_eq!(envelope.data.steps.len(), 2);
    assert!(envelope
        .data
        .steps
        .iter()
        .any(|step| step.kind == AnalysisStepKind::Transient));
    assert!(envelope
        .data
        .steps
        .iter()
        .any(|step| step.kind == AnalysisStepKind::Cfd));
    assert_eq!(envelope.data.loads[0].load_id, "load_default_fsi_seed");
    let cfd = envelope
        .data
        .cfd
        .as_ref()
        .expect("cfd domain should be populated for fsi profile");
    assert_eq!(cfd.solve_family, CfdSolveFamily::Transient);
    assert_eq!(cfd.time_profile.len(), 2);
    assert!(envelope.data.thermo_mechanical.is_none());
}

#[test]
fn analysis_validate_study_reports_invalid_study_id() {
    let _guard = analysis_test_guard();
    let root = temp_artifact_root("validate-study-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let mut spec = sample_linear_static_study_spec();
    spec.study_id = "   ".to_string();

    let envelope = analysis_validate_study_op(&spec, OperationContext::new(None, None))
        .expect("study validation should return typed output");

    assert_eq!(envelope.operation, "fea.validate_study");
    assert_eq!(envelope.op_version, "fea.validate_study/v1");
    assert!(!envelope.data.valid);
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ID_EMPTY"));
    assert!(envelope.data.issues.iter().any(|issue| {
        issue.code == "RM.FEA.STUDY.ID_EMPTY"
            && issue.message.contains("study_id must be non-empty")
    }));
    assert!(envelope
        .data
        .evidence_artifact_path
        .ends_with("validate.json"));
    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_validate_study_rejects_unused_electromagnetic_options() {
    let _guard = analysis_test_guard();
    let mut spec = sample_linear_static_study_spec();
    spec.electromagnetic_run_options = Some(AnalysisElectromagneticRunOptions::default());

    let envelope = analysis_validate_study_op(&spec, OperationContext::new(None, None))
        .expect("study validation should return typed output");

    assert!(!envelope.data.valid);
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.RUN_OPTIONS_KIND_MISMATCH"));
    assert!(envelope.data.issues.iter().any(|issue| {
        issue.code == "RM.FEA.STUDY.RUN_OPTIONS_KIND_MISMATCH"
            && issue.message.contains("solver selected by model.profile")
    }));
}

#[test]
fn analysis_validate_study_rejects_invalid_electromagnetic_options() {
    let _guard = analysis_test_guard();
    let mut spec = sample_electromagnetic_study_spec();
    spec.electromagnetic_run_options = Some(AnalysisElectromagneticRunOptions {
        sweep_enabled: true,
        sweep_frequency_hz: vec![60.0, -10.0],
        residual_target: 0.0,
        harmonic_tolerance: f64::NAN,
        harmonic_max_iterations: 0,
        ..AnalysisElectromagneticRunOptions::default()
    });

    let envelope = analysis_validate_study_op(&spec, OperationContext::new(None, None))
        .expect("study validation should return typed output");

    assert!(!envelope.data.valid);
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ELECTROMAGNETIC_RESIDUAL_TARGET_INVALID"));
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ELECTROMAGNETIC_HARMONIC_TOLERANCE_INVALID"));
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ELECTROMAGNETIC_HARMONIC_MAX_ITERATIONS_INVALID"));
    assert!(envelope
        .data
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ELECTROMAGNETIC_SWEEP_FREQUENCY_INVALID"));
    assert!(envelope.data.issues.iter().any(|issue| {
        issue.code == "RM.FEA.STUDY.ELECTROMAGNETIC_SWEEP_FREQUENCY_INVALID"
            && issue
                .message
                .contains("must contain finite positive values")
    }));
}

#[test]
fn analysis_plan_study_returns_canonical_linear_static_sequence() {
    let _guard = analysis_test_guard();
    let root = temp_artifact_root("plan-study-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let spec = sample_linear_static_study_spec();

    let envelope = analysis_plan_study_op(&spec, OperationContext::new(None, None))
        .expect("study plan should succeed");

    assert_eq!(envelope.operation, "fea.plan_study");
    assert_eq!(envelope.op_version, "fea.plan_study/v1");
    assert_eq!(envelope.data.study_id, spec.study_id);
    assert_eq!(envelope.data.model_id, spec.create_model_intent.model_id);
    assert_eq!(envelope.data.run_operation, "fea.run_linear_static");
    assert_eq!(envelope.data.run_op_version, "fea.run_linear_static/v1");
    assert!(envelope.data.electromagnetic_run_options.is_none());
    assert_eq!(
        envelope.data.operation_sequence,
        vec![
            "fea.create_model/v1".to_string(),
            "fea.validate/v1".to_string(),
            "fea.run_linear_static/v1".to_string(),
        ]
    );
    assert!(envelope.data.study_fingerprint.starts_with("sha256:"));
    assert!(envelope.data.evidence_artifact_path.ends_with("plan.json"));
    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_plan_study_surfaces_electromagnetic_run_operation_and_options() {
    let _guard = analysis_test_guard();
    let mut spec = sample_electromagnetic_study_spec();
    spec.electromagnetic_run_options = Some(AnalysisElectromagneticRunOptions {
        sweep_enabled: true,
        sweep_frequency_hz: vec![40.0, 60.0, 120.0],
        ..AnalysisElectromagneticRunOptions::default()
    });

    let envelope = analysis_plan_study_op(&spec, OperationContext::new(None, None))
        .expect("electromagnetic study plan should succeed");

    assert_eq!(envelope.data.run_operation, "fea.run_electromagnetic");
    assert_eq!(envelope.data.run_op_version, "fea.run_electromagnetic/v1");
    assert_eq!(
        envelope.data.electromagnetic_run_options,
        spec.electromagnetic_run_options
    );
}

#[test]
fn analysis_plan_study_sweep_returns_typed_plan_entries() {
    let _guard = analysis_test_guard();
    let root = temp_artifact_root("plan-study-sweep-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let sweep_spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_plan_001".to_string(),
        studies: vec![
            sample_linear_static_study_spec(),
            sample_electromagnetic_study_spec(),
        ],
        fail_fast: true,
    };

    let envelope = analysis_plan_study_sweep_op(&sweep_spec, OperationContext::new(None, None))
        .expect("study sweep plan should succeed");

    assert_eq!(envelope.operation, "fea.plan_study_sweep");
    assert_eq!(envelope.op_version, "fea.plan_study_sweep/v1");
    assert_eq!(envelope.data.sweep_id, "study_sweep_plan_001");
    assert_eq!(envelope.data.study_count, 2);
    assert_eq!(envelope.data.planned_count, 2);
    assert_eq!(envelope.data.failed_count, 0);
    assert!(envelope.data.failure_entries.is_empty());
    assert_eq!(envelope.data.plan_entries.len(), 2);
    assert!(envelope
        .data
        .plan_entries
        .iter()
        .all(|entry| entry.study_fingerprint.starts_with("sha256:")));
    assert!(envelope
        .data
        .plan_entries
        .iter()
        .any(|entry| entry.run_kind == AnalysisRunKind::LinearStatic));
    assert!(envelope
        .data
        .plan_entries
        .iter()
        .any(|entry| entry.run_kind == AnalysisRunKind::Electromagnetic));
    assert!(envelope.data.evidence_artifact_path.ends_with("plan.json"));
    assert!(PathBuf::from(&envelope.data.evidence_artifact_path).exists());
    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_plan_study_sweep_rejects_empty_study_set() {
    let _guard = analysis_test_guard();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_plan_empty".to_string(),
        studies: Vec::new(),
        fail_fast: true,
    };

    let err = analysis_plan_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect_err("empty sweep plan should be rejected");
    assert_eq!(err.operation, "fea.plan_study_sweep");
    assert_eq!(err.op_version, "fea.plan_study_sweep/v1");
    assert_eq!(err.error_code, "RM.FEA.PLAN_STUDY_SWEEP.INVALID_SPEC");
}

#[test]
fn analysis_plan_study_sweep_can_continue_on_study_failure() {
    let _guard = analysis_test_guard();
    let mut invalid = sample_linear_static_study_spec();
    invalid.study_id = "   ".to_string();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_plan_continue".to_string(),
        studies: vec![sample_linear_static_study_spec(), invalid],
        fail_fast: false,
    };

    let envelope = analysis_plan_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect("continue-on-failure sweep planning should succeed");

    assert_eq!(envelope.data.study_count, 2);
    assert_eq!(envelope.data.planned_count, 1);
    assert_eq!(envelope.data.failed_count, 1);
    assert_eq!(envelope.data.plan_entries.len(), 1);
    assert_eq!(envelope.data.failure_entries.len(), 1);
    assert_eq!(envelope.data.failure_entries[0].study_index, 1);
    assert_eq!(
        envelope.data.failure_entries[0].error_code,
        "RM.FEA.PLAN_STUDY.INVALID_SPEC"
    );
}

#[test]
fn analysis_run_study_executes_linear_static_path() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("run-study-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let spec = sample_linear_static_study_spec();

    let envelope = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("study run should succeed");

    assert_eq!(envelope.operation, "fea.run_study");
    assert_eq!(envelope.op_version, "fea.run_study/v1");
    assert_eq!(envelope.data.study_id, spec.study_id);
    assert_eq!(envelope.data.model_id, spec.create_model_intent.model_id);
    assert_eq!(envelope.data.run_kind, AnalysisRunKind::LinearStatic);
    assert_eq!(envelope.data.backend, ComputeBackend::Cpu);
    assert!(envelope.data.electromagnetic_run_options.is_none());
    assert_eq!(envelope.data.run_operation, "fea.run_linear_static");
    assert_eq!(envelope.data.run_op_version, "fea.run_linear_static/v1");
    assert_eq!(
        envelope.data.operation_sequence,
        vec![
            "fea.create_model/v1".to_string(),
            "fea.validate/v1".to_string(),
            "fea.run_linear_static/v1".to_string(),
        ]
    );
    assert!(envelope.data.study_fingerprint.starts_with("sha256:"));
    assert!(envelope.data.run_id.starts_with("run_"));
    let prep_artifact_id = envelope
        .data
        .prep_artifact_id
        .as_ref()
        .expect("study run should persist generated prep artifact id");
    assert_eq!(
        envelope.data.run_options["prep_artifact_id"].as_str(),
        Some(prep_artifact_id.as_str())
    );
    assert!(envelope.data.evidence_artifact_path.ends_with("run.json"));

    let persisted = storage::load_run_result(&envelope.data.run_id)
        .expect("run load should succeed")
        .expect("run should be persisted");
    assert_eq!(persisted.run_id, envelope.data.run_id);
    assert_eq!(persisted.run_status, envelope.data.run_status);
    assert_eq!(persisted.publishable, envelope.data.publishable);
    assert_eq!(
        persisted.solver_convergence,
        envelope.data.solver_convergence
    );
    assert_eq!(persisted.result_quality, envelope.data.result_quality);
    assert_eq!(persisted.quality_reasons, envelope.data.quality_reasons);
    assert_eq!(persisted.provenance, envelope.data.provenance);
    let render_topology = persisted
        .render_topology
        .as_ref()
        .expect("study run should persist solver render topology");
    assert_eq!(
        render_topology.schema_version,
        "analysis_render_topology/v1"
    );
    assert_eq!(render_topology.meshes.len(), 1);
    assert_eq!(
        render_topology.meshes[0].vertices.len(),
        spec.geometry.surface_meshes[0].vertices.len()
    );
    assert_eq!(
        render_topology.meshes[0].triangles.len(),
        spec.geometry.surface_meshes[0].triangles.len()
    );
    let von_mises = persisted
        .run
        .field(FEA_FIELD_STRUCTURAL_VON_MISES)
        .expect("study run should persist von Mises field");
    assert_eq!(
        von_mises.shape,
        vec![spec.geometry.surface_meshes[0].triangles.len()]
    );
    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_run_study_honors_electromagnetic_run_options() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let mut spec = sample_electromagnetic_study_spec();
    spec.electromagnetic_run_options = Some(AnalysisElectromagneticRunOptions {
        sweep_enabled: true,
        sweep_frequency_hz: vec![30.0, 60.0, 120.0],
        residual_target: 5.0e-7,
        harmonic_tolerance: 1.2345e-4,
        harmonic_max_iterations: 64,
        ..AnalysisElectromagneticRunOptions::default()
    });

    let envelope = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("electromagnetic study run should succeed");

    assert_eq!(envelope.operation, "fea.run_study");
    assert_eq!(envelope.op_version, "fea.run_study/v1");
    assert_eq!(envelope.data.run_kind, AnalysisRunKind::Electromagnetic);
    assert_eq!(envelope.data.backend, ComputeBackend::Cpu);
    let resolved_options = envelope
        .data
        .electromagnetic_run_options
        .as_ref()
        .expect("electromagnetic study should return resolved options");
    let requested_options = spec
        .electromagnetic_run_options
        .as_ref()
        .expect("test should configure electromagnetic options");
    assert_eq!(
        resolved_options.sweep_enabled,
        requested_options.sweep_enabled
    );
    assert_eq!(
        resolved_options.sweep_frequency_hz,
        requested_options.sweep_frequency_hz
    );
    assert_eq!(
        resolved_options.residual_target,
        requested_options.residual_target
    );
    assert_eq!(
        resolved_options.harmonic_tolerance,
        requested_options.harmonic_tolerance
    );
    assert_eq!(
        resolved_options.harmonic_max_iterations,
        requested_options.harmonic_max_iterations
    );
    assert_eq!(
        resolved_options.prep_artifact_id.as_deref(),
        envelope.data.prep_artifact_id.as_deref()
    );
    assert_eq!(envelope.data.run_operation, "fea.run_electromagnetic");
    assert_eq!(envelope.data.run_op_version, "fea.run_electromagnetic/v1");

    let persisted = storage::load_run_result(&envelope.data.run_id)
        .expect("run load should succeed")
        .expect("run should be persisted");
    let em_payload = persisted
        .electromagnetic_results
        .as_ref()
        .expect("electromagnetic payload should be present");
    assert_eq!(em_payload.sweep_frequency_hz.len(), 3);
    assert_eq!(em_payload.sweep_peak_flux_density.len(), 3);
    assert_eq!(em_payload.sweep_solve_quality.len(), 3);
    let harmonic_diag = persisted
        .run
        .diagnostics
        .iter()
        .find(|diag| diag.code == "FEA_EM_HARMONIC_COUPLING")
        .expect("harmonic coupling diagnostic should be present");
    assert!(harmonic_diag.message.contains("tolerance=0.00012345"));
    assert!(harmonic_diag.message.contains("iterations="));
    assert_eq!(
        persisted.solver_convergence,
        envelope.data.solver_convergence
    );
    assert_eq!(persisted.result_quality, envelope.data.result_quality);
    assert_eq!(persisted.quality_reasons, envelope.data.quality_reasons);
    assert_eq!(persisted.provenance, envelope.data.provenance);
}

#[test]
fn analysis_run_study_emits_default_electromagnetic_options_when_unspecified() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let spec = sample_electromagnetic_study_spec();

    let envelope = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("electromagnetic study run should succeed");

    assert_eq!(envelope.data.run_kind, AnalysisRunKind::Electromagnetic);
    let expected_options = AnalysisElectromagneticRunOptions {
        prep_artifact_id: envelope.data.prep_artifact_id.clone(),
        ..Default::default()
    };
    assert_eq!(
        envelope.data.electromagnetic_run_options,
        Some(expected_options)
    );
    assert_eq!(envelope.data.run_operation, "fea.run_electromagnetic");
    assert_eq!(envelope.data.run_op_version, "fea.run_electromagnetic/v1");
}

#[test]
fn analysis_run_study_sweep_executes_multiple_studies() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("run-study-sweep-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let linear = sample_linear_static_study_spec();
    let electromagnetic = sample_electromagnetic_study_spec();
    let sweep_spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_001".to_string(),
        studies: vec![linear, electromagnetic],
        fail_fast: true,
    };

    let envelope = analysis_run_study_sweep_op(&sweep_spec, OperationContext::new(None, None))
        .expect("study sweep should succeed");

    assert_eq!(envelope.operation, "fea.run_study_sweep");
    assert_eq!(envelope.op_version, "fea.run_study_sweep/v1");
    assert_eq!(envelope.data.sweep_id, "study_sweep_001");
    assert_eq!(envelope.data.study_count, 2);
    assert_eq!(envelope.data.success_count, 2);
    assert_eq!(envelope.data.failed_count, 0);
    assert!(envelope.data.failure_entries.is_empty());
    assert_eq!(envelope.data.run_entries.len(), 2);
    assert!(envelope
        .data
        .run_entries
        .iter()
        .any(|entry| entry.run_kind == AnalysisRunKind::LinearStatic));
    assert!(envelope
        .data
        .run_entries
        .iter()
        .any(|entry| entry.run_kind == AnalysisRunKind::Electromagnetic));
    assert!(envelope
        .data
        .run_entries
        .iter()
        .all(|entry| entry.run_id.starts_with("run_")));
    assert!(envelope.data.evidence_artifact_path.ends_with("run.json"));
    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_run_study_sweep_rejects_empty_study_set() {
    let _guard = analysis_test_guard();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_empty".to_string(),
        studies: Vec::new(),
        fail_fast: true,
    };

    let err = analysis_run_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect_err("empty sweep should be rejected");
    assert_eq!(err.operation, "fea.run_study_sweep");
    assert_eq!(err.op_version, "fea.run_study_sweep/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_STUDY_SWEEP.INVALID_SPEC");
}

#[test]
fn analysis_run_study_sweep_fail_fast_returns_error_on_invalid_study() {
    let _guard = analysis_test_guard();
    let mut invalid = sample_linear_static_study_spec();
    invalid.study_id = "   ".to_string();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_fail_fast".to_string(),
        studies: vec![sample_linear_static_study_spec(), invalid],
        fail_fast: true,
    };

    let err = analysis_run_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect_err("fail-fast sweep should return error");
    assert_eq!(err.operation, "fea.run_study_sweep");
    assert_eq!(err.op_version, "fea.run_study_sweep/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_STUDY_SWEEP.STUDY_FAILED");
}

#[test]
fn analysis_run_study_sweep_can_continue_on_study_failure() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let mut invalid = sample_linear_static_study_spec();
    invalid.study_id = "   ".to_string();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_continue".to_string(),
        studies: vec![sample_linear_static_study_spec(), invalid],
        fail_fast: false,
    };

    let envelope = analysis_run_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect("continue-on-failure sweep should succeed");

    assert_eq!(envelope.data.study_count, 2);
    assert_eq!(envelope.data.success_count, 1);
    assert_eq!(envelope.data.failed_count, 1);
    assert_eq!(envelope.data.run_entries.len(), 1);
    assert_eq!(envelope.data.failure_entries.len(), 1);
    assert_eq!(envelope.data.failure_entries[0].study_index, 1);
    assert_eq!(
        envelope.data.failure_entries[0].error_code,
        "RM.FEA.RUN_STUDY.INVALID_SPEC"
    );
    assert!(envelope.data.failure_entries[0].study_id.trim().is_empty());
}

#[test]
fn analysis_validate_study_sweep_reports_valid_entries_and_persists_artifact() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("validate-study-sweep-evidence");
    let _ = fs::remove_dir_all(&root);
    let env_guard = EnvVarRestoreGuard {
        key: "RUNMAT_FEA_STUDY_ARTIFACT_ROOT",
        previous: std::env::var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT").ok(),
    };
    std::env::set_var("RUNMAT_FEA_STUDY_ARTIFACT_ROOT", root.display().to_string());
    let spec = AnalysisStudySweepSpec {
        sweep_id: "study_sweep_validate_001".to_string(),
        studies: vec![
            sample_linear_static_study_spec(),
            sample_electromagnetic_study_spec(),
        ],
        fail_fast: true,
    };

    let envelope = analysis_validate_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect("study sweep validation should succeed");

    assert_eq!(envelope.operation, "fea.validate_study_sweep");
    assert_eq!(envelope.op_version, "fea.validate_study_sweep/v1");
    assert_eq!(envelope.data.sweep_id, "study_sweep_validate_001");
    assert!(envelope.data.valid);
    assert!(envelope.data.issue_codes.is_empty());
    assert_eq!(envelope.data.study_entries.len(), 2);
    assert!(envelope.data.study_entries.iter().all(|entry| entry.valid));
    assert!(envelope
        .data
        .study_entries
        .iter()
        .all(|entry| entry.issue_codes.is_empty() && entry.issues.is_empty()));
    assert!(envelope
        .data
        .evidence_artifact_path
        .ends_with("validate.json"));
    assert!(PathBuf::from(&envelope.data.evidence_artifact_path).exists());

    drop(env_guard);
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_validate_study_sweep_reports_sweep_and_study_issue_details() {
    let _guard = analysis_test_guard();
    let mut invalid = sample_linear_static_study_spec();
    invalid.study_id = "   ".to_string();
    let spec = AnalysisStudySweepSpec {
        sweep_id: "   ".to_string(),
        studies: vec![invalid],
        fail_fast: true,
    };

    let envelope = analysis_validate_study_sweep_op(&spec, OperationContext::new(None, None))
        .expect("study sweep validation should return a typed payload");

    assert_eq!(envelope.operation, "fea.validate_study_sweep");
    assert_eq!(envelope.op_version, "fea.validate_study_sweep/v1");
    assert!(!envelope.data.valid);
    assert_eq!(
        envelope.data.issue_codes,
        vec!["RM.FEA.STUDY_SWEEP.ID_EMPTY".to_string()]
    );
    assert_eq!(envelope.data.study_entries.len(), 1);
    assert!(!envelope.data.study_entries[0].valid);
    assert!(envelope.data.study_entries[0]
        .issue_codes
        .iter()
        .any(|code| code == "RM.FEA.STUDY.ID_EMPTY"));
    assert!(envelope.data.study_entries[0]
        .issues
        .iter()
        .any(|issue| issue.code == "RM.FEA.STUDY.ID_EMPTY" && !issue.message.is_empty()));
}

#[test]
fn analysis_create_model_infers_materials_and_assignments_from_geometry_evidence() {
    let _guard = analysis_test_guard();
    let geometry = sample_step_like_geometry_asset();
    let envelope = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "model_from_step_like".to_string(),
            profile: AnalysisCreateModelProfile::LinearStaticStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("create model should succeed");

    assert!(envelope
        .data
        .materials
        .iter()
        .any(|material| material.material_id == "mat_aluminum"));
    assert_eq!(envelope.data.material_assignments.len(), 2);
    assert!(envelope
        .data
        .material_assignments
        .iter()
        .all(|assignment| assignment.assigned_material_id == "mat_aluminum"));
    assert_eq!(
        envelope.data.boundary_conditions[0].region_id,
        "region_root"
    );
    assert_eq!(envelope.data.loads[0].region_id, "region_tip");
}

#[test]
fn analysis_validate_returns_typed_envelope() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let context =
        OperationContext::new(Some("trace-a1".to_string()), Some("request-a1".to_string()));
    let envelope = analysis_validate(&model, UnitSystem::Meter, &ReferenceFrame::Global, context)
        .expect("validation should pass");

    assert_eq!(envelope.operation, "fea.validate");
    assert_eq!(envelope.op_version, "fea.validate/v1");
    assert!(envelope.data.valid);
    assert_eq!(envelope.trace_id.as_deref(), Some("trace-a1"));
}

#[test]
fn analysis_validate_maps_typed_error_code() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.materials.clear();
    let context = OperationContext::new(None, None);
    let error = analysis_validate(&model, UnitSystem::Meter, &ReferenceFrame::Global, context)
        .expect_err("validation should fail");

    assert_eq!(error.error_code, "RM.FEA.VALIDATE.MISSING_MATERIALS");
    assert_eq!(error.operation, "fea.validate");
    assert_eq!(error.op_version, "fea.validate/v1");
}

#[test]
fn analysis_validate_maps_invalid_moment_error_code() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.loads[0].kind = LoadKind::Moment {
        mx: f64::INFINITY,
        my: 0.0,
        mz: 1.0,
    };
    let context = OperationContext::new(None, None);
    let error = analysis_validate(&model, UnitSystem::Meter, &ReferenceFrame::Global, context)
        .expect_err("validation should fail");

    assert_eq!(error.error_code, "RM.FEA.VALIDATE.INVALID_MOMENT");
    assert_eq!(
        error.context.get("load_id").map(String::as_str),
        Some("load_tip")
    );
}

#[test]
fn analysis_run_linear_static_returns_typed_envelope() {
    let _guard = analysis_test_guard();
    let _prep_guard = crate::geometry::prep_artifact_test_guard();
    crate::geometry::reset_prep_artifact_store_for_tests();
    let model = sample_model();
    let prep_artifact_id = sample_prep_artifact_id_for_model(&model);
    let context =
        OperationContext::new(Some("trace-a2".to_string()), Some("request-a2".to_string()));
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Cpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Auto,
            quality_policy: QualityPolicy::Balanced,
            prep_context: Some(sample_analysis_run_prep_context()),
            prep_artifact_id: Some(prep_artifact_id),
            prep_calibration_profile: None,
        },
        context,
    )
    .expect("run should pass");

    assert_eq!(envelope.operation, "fea.run_linear_static");
    assert_eq!(envelope.op_version, "fea.run_linear_static/v1");
    assert_eq!(envelope.data.run.backend, ComputeBackend::Cpu);
    assert!(!envelope
        .data
        .run
        .field(FEA_FIELD_STRUCTURAL_DISPLACEMENT)
        .expect("structural displacement field should be present")
        .is_empty());
    assert_eq!(envelope.data.run_status, RunStatus::Publishable);
    assert!(envelope.data.publishable);
    assert!(envelope.data.modal_results.is_none());
    assert_eq!(envelope.data.solver_convergence, QualityGate::Pass);
    assert!(envelope.data.provenance.deterministic_mode);
    assert_eq!(envelope.data.provenance.precision_mode, "fp64");
    assert_eq!(envelope.data.provenance.solver_method, "matrix_free_pcg");
    assert_eq!(envelope.data.provenance.preconditioner, "ilu0");
    assert_eq!(envelope.data.provenance.quality_policy, "balanced");
    assert_eq!(envelope.data.provenance.solver_device_apply_k_ratio, 0.0);
    assert_eq!(envelope.data.provenance.solver_host_sync_count, 0);
}

#[test]
fn analysis_run_linear_static_with_thermo_mechanical_coupling_reports_fields() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 65.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Cpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Auto,
            quality_policy: QualityPolicy::Balanced,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-linear-thermo-fields".to_string()), None),
    )
    .expect("thermo-mechanical linear static run should pass");

    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_temperature_field_id(0))
        .is_some());
    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_thermal_strain_field_id(0))
        .is_some());
    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_thermal_stress_field_id(0))
        .is_some());
    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_displacement_field_id(0))
        .is_some());
    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_von_mises_field_id(0))
        .is_some());
    assert!(envelope
        .data
        .run
        .field(&fea_thermo_mechanical_coupling_residual_field_id(0))
        .is_some());

    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("thermo-mechanical linear static results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("thermo-mechanical descriptor should be present")
    };
    for field_id in [
        fea_thermo_mechanical_temperature_field_id(0),
        fea_thermo_mechanical_von_mises_field_id(0),
        fea_thermo_mechanical_coupling_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    let displacement_descriptor = descriptor(&fea_thermo_mechanical_displacement_field_id(0));
    assert_eq!(displacement_descriptor.kind, AnalysisFieldKind::Vector);
    assert_eq!(displacement_descriptor.component_count, Some(3));
    for field_id in [
        fea_thermo_mechanical_thermal_strain_field_id(0),
        fea_thermo_mechanical_thermal_stress_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Tensor);
        assert_eq!(descriptor.component_count, Some(6));
    }
}

#[test]
fn analysis_run_linear_static_persists_artifacts_through_runtime_filesystem_provider() {
    let _guard = analysis_test_guard();
    let _provider_lock = runmat_filesystem::provider_override_lock();
    storage::reset_artifact_store_for_tests();
    let sandbox_root = temp_artifact_root("artifact-provider");
    let _ = fs::remove_dir_all(&sandbox_root);
    let provider = Arc::new(
        runmat_filesystem::SandboxFsProvider::new(sandbox_root.clone())
            .expect("sandbox filesystem provider should be created"),
    );
    let _provider_guard = runmat_filesystem::replace_provider(provider);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: PathBuf::from("/fea-artifacts"),
    })
    .expect("configure provider-backed filesystem artifact store");

    let model = sample_model();
    let envelope = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-provider-artifact".to_string()), None),
    )
    .expect("run should pass");
    let artifact_path = PathBuf::from("/fea-artifacts")
        .join("runs")
        .join(format!("{}.json", envelope.data.run_id));

    let bytes =
        runmat_filesystem::read(&artifact_path).expect("artifact should be provider-readable");
    assert!(!bytes.is_empty());
    let persisted = storage::load_run_result(&envelope.data.run_id)
        .expect("provider-backed artifact load should succeed")
        .expect("run artifact should exist");
    assert_eq!(persisted.run_id, envelope.data.run_id);
    assert!(sandbox_root
        .join("fea-artifacts")
        .join("runs")
        .join(format!("{}.json", envelope.data.run_id))
        .exists());

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&sandbox_root);
}

#[test]
fn analysis_run_linear_static_cancellation_maps_normalized_error() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let cancelled = Arc::new(AtomicBool::new(true));
    let _interrupt_guard = crate::interrupt::replace_interrupt(Some(cancelled));
    let model = sample_model();

    let err = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-cancel".to_string()), None),
    )
    .expect_err("cancelled run should fail");

    assert_eq!(err.error_code, "RM.FEA.RUN_LINEAR_STATIC.CANCELLED");
    assert_eq!(err.error_type, OperationErrorType::Cancelled);
    assert_eq!(err.severity, OperationErrorSeverity::Warning);
    assert!(!err.retryable);
    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_run_linear_static_emits_solver_and_artifact_progress_events() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let events = Arc::new(Mutex::new(Vec::new()));
    let events_for_handler = Arc::clone(&events);
    let _progress_guard = replace_fea_progress_handler(Some(Arc::new(move |event| {
        events_for_handler
            .lock()
            .expect("progress event lock should not be poisoned")
            .push(event);
    })));
    let model = sample_model();

    analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-progress".to_string()), None),
    )
    .expect("run should pass");

    let events = events
        .lock()
        .expect("progress event lock should not be poisoned");
    assert!(events.iter().any(|event| {
        event.operation == "fea.run_linear_static"
            && event.phase == FeaProgressPhase::Solve
            && event.status == FeaProgressStatus::Started
    }));
    assert!(events.iter().any(|event| {
        event.operation == "fea.run_linear_static"
            && event.phase == FeaProgressPhase::ArtifactPersistence
            && event.status == FeaProgressStatus::Completed
    }));
    assert!(events.iter().any(|event| {
        event.operation == "fea.run_linear_static"
            && event.phase == FeaProgressPhase::Complete
            && event.status == FeaProgressStatus::Completed
    }));

    storage::reset_artifact_store_for_tests();
}

#[test]
fn gpu_run_without_provider_records_fallback_event() {
    let _guard = analysis_test_guard();
    let _guard = runmat_accelerate_api::ThreadProviderGuard::set(None);
    let model = sample_model();
    let envelope = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Gpu,
        OperationContext::new(None, None),
    )
    .expect("run should pass");

    if envelope.data.provenance.solver_backend == "cpu_reference" {
        assert!(envelope
            .data
            .provenance
            .fallback_events
            .iter()
            .any(|event| event.starts_with("SOLVER_BACKEND_FALLBACK")));
        assert_eq!(envelope.data.provenance.solver_device_apply_k_ratio, 0.0);
        assert!(matches!(
            envelope
                .data
                .run
                .field(FEA_FIELD_STRUCTURAL_DISPLACEMENT)
                .expect("structural displacement field should be present")
                .values,
            AnalysisFieldValues::HostF64(_)
        ));
    } else {
        assert_eq!(envelope.data.provenance.solver_backend, "runtime_tensor");
    }
}

#[test]
fn gpu_run_with_provider_emits_device_refs() {
    let _guard = analysis_test_guard();
    static NEXT_BUFFER_ID: AtomicU64 = AtomicU64::new(1000);

    struct AnalysisTestProvider;

    impl AccelProvider for AnalysisTestProvider {
        fn upload(&self, host: &HostTensorView) -> anyhow::Result<GpuTensorHandle> {
            Ok(GpuTensorHandle {
                shape: host.shape.to_vec(),
                device_id: 7,
                buffer_id: NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed),
            })
        }

        fn download<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a> {
            Box::pin(async move {
                Ok(HostTensorOwned {
                    data: vec![0.0; h.shape.iter().product()],
                    shape: h.shape.clone(),
                    storage: runmat_accelerate_api::handle_storage(h),
                })
            })
        }

        fn free(&self, _h: &GpuTensorHandle) -> anyhow::Result<()> {
            Ok(())
        }

        fn device_info(&self) -> String {
            "analysis-test-provider".to_string()
        }

        fn device_id(&self) -> u32 {
            7
        }

        fn device_info_struct(&self) -> ApiDeviceInfo {
            ApiDeviceInfo {
                device_id: 7,
                name: "analysis-test-provider".to_string(),
                vendor: "runmat-tests".to_string(),
                memory_bytes: None,
                backend: Some("test_gpu".to_string()),
            }
        }
    }

    static PROVIDER: AnalysisTestProvider = AnalysisTestProvider;
    let _guard = runmat_accelerate_api::ThreadProviderGuard::set(Some(&PROVIDER));

    let model = sample_model();
    let envelope = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Gpu,
        OperationContext::new(None, None),
    )
    .expect("run should pass");

    assert!(!envelope
        .data
        .provenance
        .fallback_events
        .iter()
        .any(|event| event.starts_with("BACKEND_NO_PROVIDER")
            || event.starts_with("BACKEND_UPLOAD_FAILED")));
    assert!(matches!(
        envelope.data.provenance.solver_backend.as_str(),
        "runtime_tensor" | "cpu_reference"
    ));
    if envelope.data.provenance.solver_backend == "cpu_reference" {
        assert!(envelope
            .data
            .provenance
            .fallback_events
            .iter()
            .any(|event| event.starts_with("SOLVER_BACKEND_FALLBACK")));
    }
    assert!(
        (0.0..=1.0).contains(&envelope.data.provenance.solver_device_apply_k_ratio),
        "ratio must be in [0,1]"
    );
    assert!(matches!(
        envelope
            .data
            .run
            .field(FEA_FIELD_STRUCTURAL_DISPLACEMENT)
            .expect("structural displacement field should be present")
            .values,
        AnalysisFieldValues::DeviceRef(_)
    ));
    assert!(matches!(
        envelope
            .data
            .run
            .field(FEA_FIELD_STRUCTURAL_VON_MISES)
            .expect("structural von Mises field should be present")
            .values,
        AnalysisFieldValues::DeviceRef(_)
    ));
}

#[test]
fn analysis_results_returns_filtered_fields_and_metadata() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let run = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-results-1".to_string()), None),
    )
    .expect("run should pass");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: vec![FEA_FIELD_STRUCTURAL_DISPLACEMENT.to_string()],
            include_field_values: true,

            include_diagnostics: false,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(Some("trace-results-2".to_string()), None),
    )
    .expect("results should pass");

    assert_eq!(results.operation, "fea.results");
    assert_eq!(results.op_version, "fea.results/v1");
    assert_eq!(results.data.fields.len(), 1);
    assert_eq!(
        results.data.fields[0].field_id,
        FEA_FIELD_STRUCTURAL_DISPLACEMENT
    );
    assert!(results.data.diagnostics.is_none());
    assert_eq!(results.data.summary.field_count, 1);
    assert_eq!(results.data.summary.mode_count, 0);
    assert!(results.data.summary.available_mode_indices.is_empty());
    assert_eq!(results.data.summary.min_frequency_hz, None);
    assert_eq!(results.data.summary.max_frequency_hz, None);
    assert_eq!(results.data.summary.max_modal_residual_norm, None);
    assert_eq!(results.data.summary.first_mode_converged, None);
    assert_eq!(results.data.summary.snapshot_count, 0);
    assert_eq!(results.data.summary.time_start_s, None);
    assert_eq!(results.data.summary.time_end_s, None);
    assert_eq!(results.data.summary.max_transient_residual_norm, None);
    assert_eq!(results.data.summary.final_step_converged, None);
}

#[test]
fn analysis_field_descriptors_include_physics_units_and_locations() {
    let cases = vec![
        (
            FEA_FIELD_STRUCTURAL_DISPLACEMENT.to_string(),
            "structural",
            "displacement",
            Some("m"),
            AnalysisFieldLocation::Node,
        ),
        (
            FEA_FIELD_STRUCTURAL_STRESS.to_string(),
            "structural",
            "stress",
            Some("Pa"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_MODAL_FREQUENCY_HZ.to_string(),
            "modal",
            "frequency_hz",
            Some("Hz"),
            AnalysisFieldLocation::Mode,
        ),
        (
            fea_modal_mode_shape_field_id(1),
            "modal",
            "mode_shape",
            Some("m"),
            AnalysisFieldLocation::Node,
        ),
        (
            FEA_FIELD_ACOUSTIC_PRESSURE_REAL.to_string(),
            "acoustic",
            "pressure_real",
            Some("Pa"),
            AnalysisFieldLocation::Node,
        ),
        (
            FEA_FIELD_CFD_WALL_SHEAR_STRESS.to_string(),
            "cfd",
            "wall_shear_stress",
            Some("Pa"),
            AnalysisFieldLocation::BoundaryFace,
        ),
        (
            fea_thermal_temperature_gradient_field_id(0),
            "thermal",
            "temperature_gradient",
            Some("K/m"),
            AnalysisFieldLocation::Element,
        ),
        (
            fea_thermal_boundary_heat_flux_field_id(0),
            "thermal",
            "boundary_heat_flux",
            Some("W/m^2"),
            AnalysisFieldLocation::BoundaryFace,
        ),
        (
            FEA_FIELD_EM_VECTOR_POTENTIAL_REAL.to_string(),
            "em",
            "vector_potential_real",
            Some("Wb/m"),
            AnalysisFieldLocation::Edge,
        ),
        (
            FEA_FIELD_EM_ELECTRIC_FLUX_DENSITY_REAL.to_string(),
            "em",
            "electric_flux_density_real",
            Some("C/m^2"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT.to_string(),
            "electro_thermal",
            "joule_heat",
            Some("W/m^3"),
            AnalysisFieldLocation::Element,
        ),
        (
            fea_nonlinear_contact_pressure_field_id(0),
            "nonlinear",
            "contact_pressure",
            Some("Pa"),
            AnalysisFieldLocation::InterfaceFace,
        ),
        (
            fea_cht_interface_heat_flux_field_id(0),
            "cht",
            "interface_heat_flux",
            Some("W/m^2"),
            AnalysisFieldLocation::InterfaceFace,
        ),
        (
            fea_fsi_interface_traction_field_id(0),
            "fsi",
            "interface_traction",
            Some("Pa"),
            AnalysisFieldLocation::InterfaceFace,
        ),
        (
            fea_thermo_mechanical_thermal_strain_field_id(0),
            "thermo_mechanical",
            "thermal_strain",
            Some("1"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_STRUCTURAL_SHELL_MEMBRANE_FORCE.to_string(),
            "structural",
            "shell_membrane_force",
            Some("N"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_STRUCTURAL_SHELL_BENDING_MOMENT.to_string(),
            "structural",
            "shell_bending_moment",
            Some("N*m"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_STRUCTURAL_SHELL_TRANSVERSE_SHEAR.to_string(),
            "structural",
            "shell_transverse_shear",
            Some("N"),
            AnalysisFieldLocation::Element,
        ),
        (
            FEA_FIELD_STRUCTURAL_SHELL_VON_MISES.to_string(),
            "structural",
            "shell_von_mises",
            Some("Pa"),
            AnalysisFieldLocation::Element,
        ),
    ];

    for (field_id, family, quantity, unit, location) in cases {
        let descriptor = AnalysisFieldDescriptor::from_field(&AnalysisField::host_f64(
            &field_id,
            vec![1],
            vec![0.0],
        ));
        assert_eq!(descriptor.family, family, "{field_id} family");
        assert_eq!(descriptor.quantity, quantity, "{field_id} quantity");
        assert_eq!(descriptor.unit.as_deref(), unit, "{field_id} unit");
        assert_eq!(descriptor.location, location, "{field_id} location");
    }

    let membrane = AnalysisFieldDescriptor::from_field(&AnalysisField::host_f64(
        FEA_FIELD_STRUCTURAL_SHELL_MEMBRANE_FORCE,
        vec![1, 3],
        vec![0.0, 0.0, 0.0],
    ));
    assert_eq!(membrane.kind, AnalysisFieldKind::Vector);
    assert_eq!(membrane.component_count, Some(3));
    let shear = AnalysisFieldDescriptor::from_field(&AnalysisField::host_f64(
        FEA_FIELD_STRUCTURAL_SHELL_TRANSVERSE_SHEAR,
        vec![1, 2],
        vec![0.0, 0.0],
    ));
    assert_eq!(shear.kind, AnalysisFieldKind::Vector);
    assert_eq!(shear.component_count, Some(2));
    let von_mises = AnalysisFieldDescriptor::from_field(&AnalysisField::host_f64(
        FEA_FIELD_STRUCTURAL_SHELL_VON_MISES,
        vec![1],
        vec![0.0],
    ));
    assert_eq!(von_mises.kind, AnalysisFieldKind::Scalar);
    assert_eq!(von_mises.component_count, None);
}

#[test]
fn analysis_results_describes_structural_l2_fields() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let run = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-results-structural-fields-1".to_string()), None),
    )
    .expect("run should pass");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: vec![
                FEA_FIELD_STRUCTURAL_STRAIN.to_string(),
                FEA_FIELD_STRUCTURAL_STRESS.to_string(),
                FEA_FIELD_STRUCTURAL_REACTION_FORCE.to_string(),
                FEA_FIELD_STRUCTURAL_TOTAL_STRAIN_ENERGY.to_string(),
                FEA_FIELD_STRUCTURAL_RESIDUAL_NORM.to_string(),
                FEA_FIELD_STRUCTURAL_EQUATION_SCALE.to_string(),
            ],
            include_field_values: false,
            include_diagnostics: false,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(Some("trace-results-structural-fields-2".to_string()), None),
    )
    .expect("results should pass");

    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("descriptor should be present")
    };

    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_STRAIN).kind,
        AnalysisFieldKind::Tensor
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_STRAIN).component_count,
        Some(6)
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_STRESS).kind,
        AnalysisFieldKind::Tensor
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_STRESS).component_count,
        Some(6)
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_REACTION_FORCE).kind,
        AnalysisFieldKind::Vector
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_REACTION_FORCE).component_count,
        Some(3)
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_TOTAL_STRAIN_ENERGY).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_RESIDUAL_NORM).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_EQUATION_SCALE).kind,
        AnalysisFieldKind::Scalar
    );
}

#[test]
fn analysis_results_describes_beam_resultant_fields() {
    let _guard = analysis_test_guard();
    let model = runmat_analysis_fea::fixtures::fixture_model(
        runmat_analysis_fea::fixtures::FixtureId::StructuralBeamCantileverEndMomentReference,
    );
    let run = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-results-beam-fields-1".to_string()), None),
    )
    .expect("beam run should pass");

    assert_eq!(
        run.data
            .run
            .field(FEA_FIELD_STRUCTURAL_BEAM_BENDING_MOMENT)
            .expect("beam bending moment field should be present")
            .shape,
        vec![1, 2]
    );
    assert_eq!(
        run.data
            .run
            .field(FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS)
            .expect("beam torsion stress field should be present")
            .shape,
        vec![1]
    );

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: vec![
                FEA_FIELD_STRUCTURAL_BEAM_AXIAL_FORCE.to_string(),
                FEA_FIELD_STRUCTURAL_BEAM_SHEAR_FORCE.to_string(),
                FEA_FIELD_STRUCTURAL_BEAM_TORSION_MOMENT.to_string(),
                FEA_FIELD_STRUCTURAL_BEAM_BENDING_MOMENT.to_string(),
                FEA_FIELD_STRUCTURAL_BEAM_BENDING_STRESS.to_string(),
                FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS.to_string(),
            ],
            include_field_values: false,
            include_diagnostics: false,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(Some("trace-results-beam-fields-2".to_string()), None),
    )
    .expect("beam results should pass");

    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("descriptor should be present")
    };

    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_AXIAL_FORCE)
            .unit
            .as_deref(),
        Some("N")
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_AXIAL_FORCE).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_SHEAR_FORCE).component_count,
        Some(2)
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_TORSION_MOMENT)
            .unit
            .as_deref(),
        Some("N*m")
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_BENDING_MOMENT).kind,
        AnalysisFieldKind::Vector
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_BENDING_STRESS)
            .unit
            .as_deref(),
        Some("Pa")
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_BENDING_STRESS).component_count,
        Some(2)
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS).location,
        AnalysisFieldLocation::Element
    );
}

#[test]
fn analysis_results_unknown_field_maps_typed_error() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let run = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-results-3".to_string()), None),
    )
    .expect("run should pass");

    let err = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: vec!["strain_energy".to_string()],
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(Some("trace-results-4".to_string()), None),
    )
    .expect_err("results should fail");

    assert_eq!(err.operation, "fea.results");
    assert_eq!(err.op_version, "fea.results/v1");
    assert_eq!(err.error_code, "RM.FEA.RESULTS.FIELD_NOT_FOUND");
}

#[test]
fn analysis_results_by_run_id_roundtrip_works() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let model = sample_model();
    let run = analysis_run_linear_static_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-results-by-id-run".to_string()), None),
    )
    .expect("run should pass");

    let fetched = analysis_results_by_run_id_op(
        &run.data.run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(Some("trace-results-by-id-fetch".to_string()), None),
    )
    .expect("results by id should pass");

    assert_eq!(fetched.operation, "fea.results");
    assert_eq!(fetched.op_version, "fea.results/v1");
    assert_eq!(fetched.data.summary.field_count, 10);
    assert_eq!(fetched.data.field_descriptors.len(), 10);
    let displacement_descriptor = fetched
        .data
        .field_descriptors
        .iter()
        .find(|descriptor| descriptor.field_id == FEA_FIELD_STRUCTURAL_DISPLACEMENT)
        .expect("structural displacement descriptor should replay");
    assert_eq!(displacement_descriptor.unit.as_deref(), Some("m"));
    assert_eq!(
        displacement_descriptor.location,
        AnalysisFieldLocation::Node
    );
    let rotation_descriptor = fetched
        .data
        .field_descriptors
        .iter()
        .find(|descriptor| descriptor.field_id == FEA_FIELD_STRUCTURAL_ROTATION)
        .expect("structural rotation descriptor should replay");
    assert_eq!(rotation_descriptor.unit.as_deref(), Some("rad"));
    let reaction_moment_descriptor = fetched
        .data
        .field_descriptors
        .iter()
        .find(|descriptor| descriptor.field_id == FEA_FIELD_STRUCTURAL_REACTION_MOMENT)
        .expect("structural reaction moment descriptor should replay");
    assert_eq!(reaction_moment_descriptor.unit.as_deref(), Some("N*m"));
    assert_eq!(fetched.data.summary.mode_count, 0);
    assert!(fetched.data.summary.available_mode_indices.is_empty());
    assert_eq!(fetched.data.summary.min_frequency_hz, None);
    assert_eq!(fetched.data.summary.max_frequency_hz, None);
    assert_eq!(fetched.data.summary.max_modal_residual_norm, None);
    assert_eq!(fetched.data.summary.first_mode_converged, None);
    assert_eq!(fetched.data.summary.snapshot_count, 0);
    assert_eq!(fetched.data.summary.time_start_s, None);
    assert_eq!(fetched.data.summary.time_end_s, None);
    assert_eq!(fetched.data.summary.max_transient_residual_norm, None);
    assert_eq!(fetched.data.summary.final_step_converged, None);

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_results_by_run_id_missing_maps_typed_error() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let err = analysis_results_by_run_id_op(
        "run_missing",
        AnalysisResultsQuery::default(),
        OperationContext::new(Some("trace-results-by-id-missing".to_string()), None),
    )
    .expect_err("missing run id should fail");

    assert_eq!(err.error_code, "RM.FEA.RESULTS.RUN_NOT_FOUND");
    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_results_compare_reports_typed_deltas() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let baseline = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            max_newton_iters: 1,
            line_search: false,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("baseline nonlinear run should succeed");
    let candidate = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions::production_recommended(),
        OperationContext::new(None, None),
    )
    .expect("candidate nonlinear run should succeed");

    let compare = analysis_results_compare_op(
        AnalysisResultsCompareQuery {
            baseline_run_id: baseline.data.run_id.clone(),
            candidate_run_id: candidate.data.run_id.clone(),
        },
        OperationContext::new(None, None),
    )
    .expect("compare operation should succeed");

    assert_eq!(compare.operation, "fea.results_compare");
    assert_eq!(compare.op_version, "fea.results_compare/v1");
    assert!(compare.data.failed_increment_delta.is_some());
    assert!(compare.data.max_iteration_delta.is_some());
    assert!(compare.data.solve_ms_delta.is_some());

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_trends_summarizes_recent_nonlinear_runs() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    for _ in 0..4 {
        let _ = analysis_run_nonlinear_op(
            &model,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("nonlinear run should persist for trends");
    }

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 3 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed");

    assert_eq!(trends.operation, "fea.trends");
    assert_eq!(trends.op_version, "fea.trends/v1");
    let nonlinear = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Nonlinear)
        .expect("nonlinear trend summary should exist");
    assert_eq!(nonlinear.sample_count, 3);
    assert!(nonlinear.median_solve_ms.is_some());
    assert!(nonlinear.p95_solve_ms.is_some());
    assert!(nonlinear.failed_increment_rate.is_some());
    assert!(nonlinear.thermo_coupling_enabled_rate.is_none());
    assert!(nonlinear.thermo_transient_warn_rate.is_none());
    assert!(nonlinear.thermo_nonlinear_warn_rate.is_none());
    assert!(nonlinear.thermo_spread_breach_rate.is_none());
    assert!(nonlinear.thermo_heterogeneity_breach_rate.is_none());
    assert!(nonlinear.electro_thermal_coupling_enabled_rate.is_none());
    assert!(nonlinear.electro_transient_warn_rate.is_none());
    assert!(nonlinear.electro_nonlinear_warn_rate.is_none());
    assert!(nonlinear.plastic_nonlinear_warn_rate.is_none());
    assert!(nonlinear.contact_nonlinear_warn_rate.is_none());

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_trends_classifies_acoustic_runs_separately() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let geometry = sample_geometry_asset();
    let acoustic_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_trend_model".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created");
    for _ in 0..3 {
        let _ = analysis_run_acoustic_op(
            &acoustic_model.data,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("acoustic run should persist for trends");
    }

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 2 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed");

    let acoustic = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Acoustic)
        .expect("acoustic trend summary should exist");
    assert_eq!(acoustic.sample_count, 2);
    assert!(acoustic.median_solve_ms.is_some());
    assert!(acoustic.p95_solve_ms.is_some());

    let modal = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Modal);
    assert!(
        modal.is_none(),
        "acoustic runs should not classify as modal"
    );

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_trends_classifies_cfd_runs_separately() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "cfd_1".to_string(),
        kind: AnalysisStepKind::Cfd,
    }];
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));
    for _ in 0..3 {
        let _ = analysis_run_cfd_op(
            &model,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("cfd run should persist for trends");
    }

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 2 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed");

    let cfd = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Cfd)
        .expect("cfd trend summary should exist");
    assert_eq!(cfd.sample_count, 2);
    assert!(cfd.median_solve_ms.is_some());
    assert!(cfd.p95_solve_ms.is_some());
    assert!(cfd.failed_increment_rate.is_none());

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_trends_classifies_cht_runs_separately() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let model = sample_cht_model();
    for _ in 0..3 {
        let _ = analysis_run_cht_op(
            &model,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("cht run should persist for trends");
    }

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 2 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed");

    let cht = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Cht)
        .expect("cht trend summary should exist");
    assert_eq!(cht.sample_count, 2);
    assert!(cht.median_solve_ms.is_some());
    assert!(cht.p95_solve_ms.is_some());
    assert!(cht.failed_increment_rate.is_none());

    let cfd = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Cfd);
    assert!(cfd.is_none(), "cht runs should not classify as cfd");

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_trends_classifies_fsi_runs_separately() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();

    let model = sample_fsi_model();
    for _ in 0..3 {
        let _ = analysis_run_fsi_op(
            &model,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("fsi run should persist for trends");
    }

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 2 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed");

    let fsi = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Fsi)
        .expect("fsi trend summary should exist");
    assert_eq!(fsi.sample_count, 2);
    assert!(fsi.median_solve_ms.is_some());
    assert!(fsi.p95_solve_ms.is_some());
    assert!(fsi.failed_increment_rate.is_none());

    let transient = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Transient);
    assert!(
        transient.is_none(),
        "fsi runs should not classify as transient"
    );

    storage::reset_artifact_store_for_tests();
}

#[test]
fn analysis_results_summary_surfaces_thermo_transient_metrics() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 65.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );
    set_model_electro_coupling(
        &mut model,
        ElectroThermalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: 36.0,
            base_electrical_conductivity_s_per_m: 3.5e7,
            resistive_heating_coefficient: 4.0e-4,
            region_conductivity_scales: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect("transient run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    assert_eq!(results.data.summary.thermo_coupling_enabled, Some(true));
    assert!(results.data.summary.thermo_coupling_fingerprint.is_some());
    assert!(results
        .data
        .summary
        .thermo_constitutive_temperature_factor
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_effective_modulus_scale
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_constitutive_material_spread_ratio
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_assignment_heterogeneity_index
        .is_some());
    assert!(results.data.summary.thermo_transient_severity.is_some());
    assert!(results.data.summary.thermo_nonlinear_severity.is_none());
    assert_eq!(
        results.data.summary.electro_thermal_coupling_enabled,
        Some(true)
    );
    assert!(results
        .data
        .summary
        .electro_thermal_coupling_fingerprint
        .is_some());
    assert!(results.data.summary.electro_joule_heating_scale.is_some());
    assert!(results
        .data
        .summary
        .electro_conductivity_spread_ratio
        .is_some());
    assert!(results.data.summary.electro_transient_severity.is_some());
    assert!(results.data.summary.electro_nonlinear_severity.is_none());
    assert!(results.data.summary.plastic_nonlinear_severity.is_none());
    assert!(results.data.summary.contact_nonlinear_severity.is_none());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT)
        .is_some());
    let electric_field = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD)
        .expect("electro-thermal electric field should be present");
    let current_density = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY)
        .expect("electro-thermal current density should be present");
    let joule_heat = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT)
        .expect("electro-thermal Joule heat should be present");
    assert_eq!(electric_field.shape.len(), 2);
    assert_eq!(electric_field.shape[1], 3);
    assert_eq!(current_density.shape, electric_field.shape);
    assert_eq!(joule_heat.shape, vec![electric_field.shape[0]]);
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("electro-thermal descriptor should be present")
    };
    assert_eq!(
        descriptor(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL).kind,
        AnalysisFieldKind::Scalar
    );
    for field_id in [
        FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD,
        FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY,
    ] {
        let descriptor = descriptor(field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
    assert_eq!(
        descriptor(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT).component_count,
        None
    );
    let transient = run
        .data
        .transient_results
        .as_ref()
        .expect("transient results should be present");
    assert_eq!(
        transient.electro_thermal_temperature_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.electro_thermal_thermal_residual_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.electro_thermal_temperature_snapshots[0].field_id,
        fea_electro_thermal_temperature_field_id(0)
    );
    assert_eq!(
        transient.electro_thermal_thermal_residual_snapshots[0].field_id,
        fea_electro_thermal_thermal_residual_field_id(0)
    );
    for field_id in [
        fea_electro_thermal_temperature_field_id(0),
        fea_electro_thermal_thermal_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
}

#[test]
fn analysis_run_transient_rejects_invalid_electro_thermal_voltage() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_invalid_electro_voltage".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_electro_coupling(
        &mut model,
        ElectroThermalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: f64::NAN,
            base_electrical_conductivity_s_per_m: 3.5e7,
            resistive_heating_coefficient: 4.0e-4,
            region_conductivity_scales: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let err = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("transient run should reject invalid electro-thermal voltage");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_TRANSIENT.INVALID_ELECTRO_THERMAL_OPTIONS"
    );
    assert_eq!(
        err.context.get("applied_voltage_v").map(String::as_str),
        Some("NaN")
    );
}

#[test]
fn analysis_run_transient_rejects_invalid_electro_thermal_conductivity_scale() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_invalid_electro_conductivity".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_electro_coupling(
        &mut model,
        ElectroThermalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: 36.0,
            base_electrical_conductivity_s_per_m: 3.5e7,
            resistive_heating_coefficient: 4.0e-4,
            region_conductivity_scales: vec![ElectroRegionConductivityScale {
                region_id: "tip".to_string(),
                conductivity_scale: 0.0,
            }],
            time_profile: Vec::new(),
        },
    );

    let err = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("transient run should reject invalid electro-thermal conductivity scale");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_TRANSIENT.INVALID_ELECTRO_THERMAL_OPTIONS"
    );
    assert_eq!(
        err.context.get("conductivity_scale").map(String::as_str),
        Some("0")
    );
}

#[test]
fn analysis_run_transient_rejects_unmapped_electro_thermal_region() {
    let _guard = analysis_test_guard();
    let mut model = sample_model_with_material_assignment_mismatch();
    model.steps = vec![AnalysisStep {
        step_id: "transient_unmapped_electro_region".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_electro_coupling(
        &mut model,
        ElectroThermalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: 36.0,
            base_electrical_conductivity_s_per_m: 3.5e7,
            resistive_heating_coefficient: 4.0e-4,
            region_conductivity_scales: vec![ElectroRegionConductivityScale {
                region_id: "not_a_model_region".to_string(),
                conductivity_scale: 1.0,
            }],
            time_profile: Vec::new(),
        },
    );

    let err = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("transient run should reject unmapped electro-thermal regions");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_TRANSIENT.INVALID_ELECTRO_THERMAL_OPTIONS"
    );
    assert_eq!(
        err.context.get("region_id").map(String::as_str),
        Some("not_a_model_region")
    );
}

#[test]
fn analysis_results_summary_surfaces_thermo_nonlinear_metrics() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 80.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );
    set_model_electro_coupling(
        &mut model,
        ElectroThermalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_voltage_v: 82.0,
            base_electrical_conductivity_s_per_m: 2.6e7,
            resistive_heating_coefficient: 6.0e-4,
            region_conductivity_scales: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions::production_recommended(),
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    assert_eq!(results.data.summary.thermo_coupling_enabled, Some(true));
    assert!(results.data.summary.thermo_coupling_fingerprint.is_some());
    assert!(results
        .data
        .summary
        .thermo_constitutive_temperature_factor
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_effective_modulus_scale
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_constitutive_material_spread_ratio
        .is_some());
    assert!(results
        .data
        .summary
        .thermo_assignment_heterogeneity_index
        .is_some());
    assert!(results.data.summary.thermo_nonlinear_severity.is_some());
    assert!(results.data.summary.thermo_transient_severity.is_some());
    assert_eq!(
        results.data.summary.electro_thermal_coupling_enabled,
        Some(true)
    );
    assert!(results
        .data
        .summary
        .electro_thermal_coupling_fingerprint
        .is_some());
    assert!(results.data.summary.electro_joule_heating_scale.is_some());
    assert!(results
        .data
        .summary
        .electro_conductivity_spread_ratio
        .is_some());
    assert!(results.data.summary.electro_nonlinear_severity.is_some());
    assert!(results.data.summary.electro_transient_severity.is_some());
    assert!(results.data.summary.plastic_nonlinear_severity.is_none());
    assert!(results.data.summary.contact_nonlinear_severity.is_none());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY)
        .is_some());
    assert!(run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT)
        .is_some());
    let electric_field = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD)
        .expect("electro-thermal electric field should be present");
    let current_density = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY)
        .expect("electro-thermal current density should be present");
    let joule_heat = run
        .data
        .run
        .field(FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT)
        .expect("electro-thermal Joule heat should be present");
    assert_eq!(electric_field.shape.len(), 2);
    assert_eq!(electric_field.shape[1], 3);
    assert_eq!(current_density.shape, electric_field.shape);
    assert_eq!(joule_heat.shape, vec![electric_field.shape[0]]);
    let nonlinear = run
        .data
        .nonlinear_results
        .as_ref()
        .expect("nonlinear results should be present");
    assert_eq!(
        nonlinear.electro_thermal_temperature_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.electro_thermal_thermal_residual_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.electro_thermal_temperature_snapshots[0].field_id,
        fea_electro_thermal_temperature_field_id(0)
    );
    assert_eq!(
        nonlinear.electro_thermal_thermal_residual_snapshots[0].field_id,
        fea_electro_thermal_thermal_residual_field_id(0)
    );
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("nonlinear electro-thermal descriptor should be present")
    };
    for field_id in [
        FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL.to_string(),
        FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT.to_string(),
        fea_electro_thermal_temperature_field_id(0),
        fea_electro_thermal_thermal_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    for field_id in [
        FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD,
        FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY,
    ] {
        let descriptor = descriptor(field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
}

#[test]
fn analysis_trends_handles_mixed_schema_and_noisy_samples() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("trends-mixed-schema");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let run = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("seed nonlinear run should succeed");

    let run_path = root.join("runs").join(format!("{}.json", run.data.run_id));
    let raw = fs::read_to_string(&run_path).expect("read wrapped artifact");
    let wrapped: serde_json::Value = serde_json::from_str(&raw).expect("parse wrapped artifact");
    let mut legacy = wrapped
        .get("run")
        .cloned()
        .expect("wrapped artifact should have run payload");
    legacy["run_id"] = serde_json::json!(format!("{}_legacy", run.data.run_id));
    fs::write(
        root.join("runs")
            .join(format!("{}_legacy.json", run.data.run_id)),
        serde_json::to_vec_pretty(&legacy).expect("encode legacy artifact"),
    )
    .expect("write legacy artifact");

    let trends = analysis_trends_op(
        AnalysisTrendsQuery { window_size: 8 },
        OperationContext::new(None, None),
    )
    .expect("trends should succeed on mixed schema artifacts");
    let nonlinear = trends
        .data
        .summaries
        .iter()
        .find(|summary| summary.run_kind == AnalysisRunKind::Nonlinear)
        .expect("nonlinear summary should be present");
    assert!(nonlinear.sample_count >= 2);
    assert!(nonlinear.p95_solve_ms.unwrap_or(0.0) >= nonlinear.median_solve_ms.unwrap_or(0.0));

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

fn temp_artifact_root(test_name: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "runmat-analysis-tests-{}-{}",
        test_name,
        Utc::now().timestamp_nanos_opt().unwrap_or_default()
    ))
}

struct EnvVarRestoreGuard {
    key: &'static str,
    previous: Option<String>,
}

impl Drop for EnvVarRestoreGuard {
    fn drop(&mut self) {
        if let Some(previous) = self.previous.as_ref() {
            std::env::set_var(self.key, previous);
        } else {
            std::env::remove_var(self.key);
        }
    }
}

fn scoped_thermo_field_artifact_root(root: &Path) -> EnvVarRestoreGuard {
    const KEY: &str = "RUNMAT_THERMO_FIELD_ARTIFACT_ROOT";
    let previous = std::env::var(KEY).ok();
    std::env::set_var(KEY, root.display().to_string());
    EnvVarRestoreGuard { key: KEY, previous }
}

#[test]
fn analysis_results_by_run_id_legacy_nonlinear_artifacts_remain_loadable() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("legacy-loadable");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let run = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");
    let run_id = run.data.run_id.clone();
    let run_path = root.join("runs").join(format!("{run_id}.json"));

    let mut legacy_value = serde_json::to_value(&run.data).expect("serialize nonlinear run");
    let nonlinear = legacy_value
        .get_mut("nonlinear_results")
        .and_then(|value| value.as_object_mut())
        .expect("nonlinear results should be object");
    nonlinear.remove("increment_norms");
    nonlinear.remove("iteration_counts");
    nonlinear.remove("failed_increments");
    nonlinear.remove("line_search_backtracks");
    nonlinear.remove("max_line_search_backtracks_per_increment");
    nonlinear.remove("tangent_rebuild_count");
    nonlinear.remove("iteration_spike_count");
    nonlinear.remove("convergence_stall_count");
    nonlinear.remove("backtrack_burst_count");
    fs::write(
        &run_path,
        serde_json::to_vec_pretty(&legacy_value).expect("encode legacy artifact"),
    )
    .expect("write legacy artifact");

    let fetched = analysis_results_by_run_id_op(
        &run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("legacy nonlinear artifact should still load");
    assert_eq!(fetched.data.summary.failed_increment_count, Some(0));
    assert_eq!(
        fetched.data.summary.nonlinear_iteration_spike_count,
        Some(0)
    );

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_results_by_run_id_future_artifact_extra_fields_are_ignored() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("future-extra-fields");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let run = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");
    let run_id = run.data.run_id.clone();
    let run_path = root.join("runs").join(format!("{run_id}.json"));

    let mut wrapped = serde_json::json!({
        "schema_version": "analysis_run_artifact/v1",
        "created_at": Utc::now().to_rfc3339(),
        "op_version": "fea.run_nonlinear/v1",
        "run": run.data,
        "future_metadata": {
            "schema_hint": "analysis_run_artifact/v2",
            "opaque": [1, 2, 3]
        }
    });
    wrapped["run"]["nonlinear_results"]["future_spatial_difficulty"] =
        serde_json::json!([0.1, 0.2, 0.3]);
    fs::write(
        &run_path,
        serde_json::to_vec_pretty(&wrapped).expect("encode future artifact"),
    )
    .expect("write future artifact");

    let fetched = analysis_results_by_run_id_op(
        &run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("future nonlinear artifact should still load");
    assert!(fetched.data.summary.increment_count > 0);
    assert!(fetched.data.summary.max_nonlinear_iteration_count.is_some());

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_artifacts_record_family_specific_op_versions_for_coupled_runs() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("family-op-version");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");

    let mut cfd_model = sample_model();
    cfd_model.steps[0].kind = AnalysisStepKind::Cfd;
    cfd_model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));
    let cfd = analysis_run_cfd_op(
        &cfd_model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("cfd run should succeed");

    let cht = analysis_run_cht_op(
        &sample_cht_model(),
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("cht run should succeed");

    let fsi = analysis_run_fsi_op(
        &sample_fsi_model(),
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("fsi run should succeed");
    let acoustic_model = analysis_create_model_op(
        &sample_geometry_asset(),
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_op_version_model".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created");
    let acoustic = analysis_run_acoustic_op(
        &acoustic_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("acoustic run should succeed");

    let read_op_version = |run_id: &str| -> String {
        let path = root.join("runs").join(format!("{run_id}.json"));
        let raw = fs::read_to_string(path).expect("read wrapped artifact");
        let wrapped: serde_json::Value =
            serde_json::from_str(&raw).expect("parse wrapped artifact");
        wrapped
            .get("op_version")
            .and_then(|value| value.as_str())
            .expect("wrapped artifact should include op_version")
            .to_string()
    };

    assert_eq!(read_op_version(&cfd.data.run_id), "fea.run_cfd/v1");
    assert_eq!(read_op_version(&cht.data.run_id), "fea.run_cht/v1");
    assert_eq!(read_op_version(&fsi.data.run_id), "fea.run_fsi/v1");
    assert_eq!(
        read_op_version(&acoustic.data.run_id),
        "fea.run_acoustic/v1"
    );

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_artifact_retention_prunes_old_runs_per_kind() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("retention-prune");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");
    std::env::set_var("RUNMAT_FEA_ARTIFACT_MAX_RUNS_PER_KIND", "2");
    std::env::remove_var("RUNMAT_FEA_ARTIFACT_MAX_RUNS");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let mut run_ids = Vec::new();
    for _ in 0..5 {
        let run = analysis_run_nonlinear_op(
            &model,
            ComputeBackend::Cpu,
            OperationContext::new(None, None),
        )
        .expect("nonlinear run should succeed");
        run_ids.push(run.data.run_id.clone());
    }

    let run_dir = root.join("runs");
    let kept_files = fs::read_dir(&run_dir)
        .expect("read run dir")
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("json"))
        .count();
    assert!(kept_files <= 2);
    assert!(storage::load_run_result(&run_ids[0])
        .expect("load pruned result")
        .is_none());
    assert!(
        storage::load_run_result(run_ids.last().expect("latest run id"))
            .expect("load latest result")
            .is_some()
    );

    std::env::remove_var("RUNMAT_FEA_ARTIFACT_MAX_RUNS_PER_KIND");
    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn analysis_results_by_run_id_filesystem_replay_is_stable() {
    let _guard = analysis_test_guard();
    storage::reset_artifact_store_for_tests();
    let root = temp_artifact_root("filesystem-replay");
    let _ = fs::remove_dir_all(&root);
    storage::configure_artifact_store(storage::AnalysisArtifactStoreConfig::Filesystem {
        root: root.clone(),
    })
    .expect("configure filesystem artifact store");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let run = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");

    let first = analysis_results_by_run_id_op(
        &run.data.run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("load first replay");
    let second = analysis_results_by_run_id_op(
        &run.data.run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("load second replay");

    assert_eq!(first.data.summary, second.data.summary);
    assert_eq!(first.data.run_status, second.data.run_status);
    assert_eq!(first.data.publishable, second.data.publishable);
    assert_eq!(first.data.quality_reasons, second.data.quality_reasons);

    storage::reset_artifact_store_for_tests();
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn requested_preconditioner_fallback_is_recorded() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Cpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Amg,
            quality_policy: QualityPolicy::Balanced,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-preconditioner-fallback".to_string()), None),
    )
    .expect("run should succeed");

    assert_eq!(envelope.data.provenance.preconditioner, "jacobi");
    assert!(envelope
        .data
        .provenance
        .fallback_events
        .iter()
        .any(|event| event.starts_with("SOLVER_PRECONDITIONER_FALLBACK")));
}

#[test]
fn ilu_preconditioner_request_is_honored_without_fallback() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Cpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Ilu,
            quality_policy: QualityPolicy::Balanced,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-preconditioner-ilu".to_string()), None),
    )
    .expect("run should succeed");

    assert_eq!(envelope.data.provenance.preconditioner, "ilu0");
    assert!(!envelope
        .data
        .provenance
        .fallback_events
        .iter()
        .any(|event| event.starts_with("SOLVER_PRECONDITIONER_FALLBACK")));
}

#[test]
fn quality_policy_exploratory_allows_publishable_warn_path() {
    let _guard = analysis_test_guard();
    let model = runmat_analysis_fea::fixtures::fixture_model(
        runmat_analysis_fea::fixtures::FixtureId::MultiMaterialAssembly,
    );
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Cpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Auto,
            quality_policy: QualityPolicy::Exploratory,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-quality-policy-exploratory".to_string()), None),
    )
    .expect("run should succeed");

    assert!(envelope.data.publishable);
    assert_eq!(envelope.data.run_status, RunStatus::Publishable);
    assert!(envelope
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::MaterialAssignmentConflict));
    assert_eq!(envelope.data.provenance.quality_policy, "exploratory");
}

#[test]
fn quality_policy_balanced_allows_publishable_with_quality_reasons() {
    let _guard = analysis_test_guard();

    struct UploadFailProvider;

    impl AccelProvider for UploadFailProvider {
        fn upload(&self, _host: &HostTensorView) -> anyhow::Result<GpuTensorHandle> {
            Err(anyhow::anyhow!("forced-upload-failure"))
        }

        fn download<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a> {
            Box::pin(async move {
                Ok(HostTensorOwned {
                    data: vec![0.0; h.shape.iter().product()],
                    shape: h.shape.clone(),
                    storage: runmat_accelerate_api::handle_storage(h),
                })
            })
        }

        fn free(&self, _h: &GpuTensorHandle) -> anyhow::Result<()> {
            Ok(())
        }

        fn device_info(&self) -> String {
            "upload-fail-provider".to_string()
        }
    }

    static PROVIDER: UploadFailProvider = UploadFailProvider;
    let _provider_guard = runmat_accelerate_api::ThreadProviderGuard::set(Some(&PROVIDER));

    let model = sample_model();
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Gpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Auto,
            quality_policy: QualityPolicy::Balanced,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-quality-policy-balanced".to_string()), None),
    )
    .expect("run should succeed");

    assert_eq!(envelope.data.solver_convergence, QualityGate::Pass);
    assert_eq!(envelope.data.result_quality, QualityGate::Pass);
    assert!(envelope.data.publishable);
    assert_eq!(envelope.data.run_status, RunStatus::Publishable);
    assert!(envelope
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::FieldPromotionFallback));
    assert_eq!(envelope.data.provenance.quality_policy, "balanced");
}

#[test]
fn quality_policy_strict_rejects_publishable_with_quality_reasons() {
    let _guard = analysis_test_guard();

    struct UploadFailProvider;

    impl AccelProvider for UploadFailProvider {
        fn upload(&self, _host: &HostTensorView) -> anyhow::Result<GpuTensorHandle> {
            Err(anyhow::anyhow!("forced-upload-failure"))
        }

        fn download<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a> {
            Box::pin(async move {
                Ok(HostTensorOwned {
                    data: vec![0.0; h.shape.iter().product()],
                    shape: h.shape.clone(),
                    storage: runmat_accelerate_api::handle_storage(h),
                })
            })
        }

        fn free(&self, _h: &GpuTensorHandle) -> anyhow::Result<()> {
            Ok(())
        }

        fn device_info(&self) -> String {
            "upload-fail-provider".to_string()
        }
    }

    static PROVIDER: UploadFailProvider = UploadFailProvider;
    let _provider_guard = runmat_accelerate_api::ThreadProviderGuard::set(Some(&PROVIDER));

    let model = sample_model();
    let envelope = analysis_run_linear_static_with_options(
        &model,
        ComputeBackend::Gpu,
        AnalysisRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            preconditioner_mode: PreconditionerMode::Auto,
            quality_policy: QualityPolicy::Strict,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(Some("trace-quality-policy-strict".to_string()), None),
    )
    .expect("run should succeed");

    assert_eq!(envelope.data.solver_convergence, QualityGate::Pass);
    assert_eq!(envelope.data.result_quality, QualityGate::Pass);
    assert!(!envelope.data.publishable);
    assert_eq!(envelope.data.run_status, RunStatus::Degraded);
    assert!(envelope
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::FieldPromotionFallback));
    assert_eq!(envelope.data.provenance.quality_policy, "strict");
}

#[test]
fn analysis_run_modal_rejects_models_without_modal_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_modal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("modal run should fail for missing modal step");

    assert_eq!(err.operation, "fea.run_modal");
    assert_eq!(err.op_version, "fea.run_modal/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_MODAL.INVALID_MODEL");
}

#[test]
fn analysis_run_acoustic_rejects_models_without_modal_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_acoustic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("acoustic run should fail for missing acoustic harmonic step marker");

    assert_eq!(err.operation, "fea.run_acoustic");
    assert_eq!(err.op_version, "fea.run_acoustic/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_ACOUSTIC.INVALID_MODEL");
}

#[test]
fn analysis_run_acoustic_rejects_models_without_acoustic_source() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let mut acoustic_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_model_missing_source".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created")
    .data;
    acoustic_model.loads = vec![LoadCase {
        load_id: "load_structural_force_not_acoustic_source".to_string(),
        region_id: "region_default".to_string(),
        kind: LoadKind::Force {
            fx: 1.0,
            fy: 0.0,
            fz: 0.0,
        },
    }];

    let err = analysis_run_acoustic_op(
        &acoustic_model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("acoustic run should reject models without acoustic pressure sources");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ACOUSTIC.MISSING_ACOUSTIC_SOURCE"
    );
}

#[test]
fn analysis_run_acoustic_rejects_moment_loads() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let mut acoustic_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_model_with_moment".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created")
    .data;
    acoustic_model.loads = vec![sample_moment_load("acoustic_moment")];

    let err = analysis_run_acoustic_op(
        &acoustic_model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("acoustic run should reject structural moment loads");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ACOUSTIC.INVALID_ACOUSTIC_SOURCE"
    );
}

#[test]
fn analysis_run_acoustic_rejects_models_without_acoustic_boundary() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let mut acoustic_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_model_missing_boundary".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created")
    .data;
    acoustic_model.boundary_conditions = vec![BoundaryCondition {
        bc_id: "bc_structural_fixed_not_acoustic_boundary".to_string(),
        region_id: "region_default".to_string(),
        kind: BoundaryConditionKind::Fixed,
    }];

    let err = analysis_run_acoustic_op(
        &acoustic_model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("acoustic run should reject models without acoustic boundary conditions");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ACOUSTIC.MISSING_ACOUSTIC_BOUNDARY"
    );
}

#[test]
fn analysis_run_transient_rejects_models_without_transient_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("transient run should fail for missing transient step");

    assert_eq!(err.operation, "fea.run_transient");
    assert_eq!(err.op_version, "fea.run_transient/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_TRANSIENT.INVALID_MODEL");
}

#[test]
fn analysis_run_cfd_rejects_models_without_cfd_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_cfd_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cfd run should fail for missing cfd step");

    assert_eq!(err.operation, "fea.run_cfd");
    assert_eq!(err.op_version, "fea.run_cfd/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CFD.INVALID_MODEL");
}

#[test]
fn analysis_run_cfd_rejects_model_without_cfd_domain() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    let err = analysis_run_cfd_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cfd run should fail when cfd domain is missing");

    assert_eq!(err.operation, "fea.run_cfd");
    assert_eq!(err.op_version, "fea.run_cfd/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CFD.INVALID_MODEL");
}

#[test]
fn analysis_run_cfd_rejects_disabled_cfd_domain() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, false));
    let err = analysis_run_cfd_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cfd run should fail for disabled cfd domain");

    assert_eq!(err.operation, "fea.run_cfd");
    assert_eq!(err.op_version, "fea.run_cfd/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CFD.INVALID_OPTIONS");
}

#[test]
fn analysis_run_cfd_rejects_moment_loads() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));
    model.loads = vec![sample_moment_load("cfd_moment")];

    let err = analysis_run_cfd_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cfd run should reject structural moment loads");

    assert_eq!(err.operation, "fea.run_cfd");
    assert_eq!(err.op_version, "fea.run_cfd/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CFD.INVALID_LOAD");
}

#[test]
fn analysis_run_thermal_rejects_models_without_thermal_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_thermal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("thermal run should fail for missing thermal step");

    assert_eq!(err.operation, "fea.run_thermal");
    assert_eq!(err.op_version, "fea.run_thermal/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_THERMAL.INVALID_MODEL");
}

#[test]
fn analysis_run_thermal_rejects_invalid_thermal_material_values() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_invalid_material".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    model.materials[0].thermal.conductivity_w_per_mk = 0.0;

    let err = analysis_run_thermal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("thermal run should reject nonpositive thermal material values");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_THERMAL.INVALID_THERMAL_MATERIAL"
    );
}

#[test]
fn analysis_run_thermal_rejects_invalid_heat_source_values() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_invalid_source".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    model.loads = vec![LoadCase {
        load_id: "load_invalid_heat_source".to_string(),
        region_id: "thermal_core".to_string(),
        kind: LoadKind::HeatSource {
            volumetric_w_per_m3: f64::INFINITY,
        },
    }];

    let err = analysis_run_thermal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("thermal run should reject non-finite heat source values");

    assert_eq!(err.error_code, "RM.FEA.RUN_THERMAL.INVALID_THERMAL_SOURCE");
}

#[test]
fn analysis_run_thermal_rejects_moment_loads() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_moment".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    model.loads = vec![sample_moment_load("thermal_moment")];

    let err = analysis_run_thermal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("thermal run should reject structural moment loads");

    assert_eq!(err.error_code, "RM.FEA.RUN_THERMAL.INVALID_THERMAL_SOURCE");
}

#[test]
fn analysis_run_thermal_rejects_invalid_thermal_boundary_values() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_invalid_boundary".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    model.boundary_conditions = vec![BoundaryCondition {
        bc_id: "bc_invalid_convection".to_string(),
        region_id: "thermal_open_wall".to_string(),
        kind: BoundaryConditionKind::ThermalConvection {
            ambient_temperature_k: 293.15,
            coefficient_w_per_m2k: f64::NAN,
        },
    }];

    let err = analysis_run_thermal_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("thermal run should reject non-finite thermal boundary values");

    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_THERMAL.INVALID_THERMAL_BOUNDARY"
    );
}

#[test]
fn analysis_run_cht_rejects_models_without_cfd_step() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_1".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 50.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );
    let err = analysis_run_cht_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cht run should fail for missing cfd step");

    assert_eq!(err.operation, "fea.run_cht");
    assert_eq!(err.op_version, "fea.run_cht/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CHT.INVALID_MODEL");
}

#[test]
fn analysis_run_cht_rejects_models_without_thermal_step() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "cfd_1".to_string(),
        kind: AnalysisStepKind::Cfd,
    }];
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::Transient, true));
    let err = analysis_run_cht_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cht run should fail for missing thermal step");

    assert_eq!(err.operation, "fea.run_cht");
    assert_eq!(err.op_version, "fea.run_cht/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CHT.INVALID_MODEL");
}

#[test]
fn analysis_run_cht_rejects_invalid_cfd_domain_parameters() {
    let _guard = analysis_test_guard();
    let mut model = sample_cht_model();
    model
        .cfd
        .as_mut()
        .expect("cht model should include cfd")
        .dynamic_viscosity_pa_s = 0.0;
    let err = analysis_run_cht_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cht run should fail for invalid cfd domain values");

    assert_eq!(err.operation, "fea.run_cht");
    assert_eq!(err.op_version, "fea.run_cht/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CHT.INVALID_OPTIONS");
}

#[test]
fn analysis_run_cht_rejects_moment_loads() {
    let _guard = analysis_test_guard();
    let mut model = sample_cht_model();
    model.loads = vec![sample_moment_load("cht_moment")];

    let err = analysis_run_cht_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cht run should reject structural moment loads");

    assert_eq!(err.operation, "fea.run_cht");
    assert_eq!(err.op_version, "fea.run_cht/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CHT.INVALID_LOAD");
}

#[test]
fn analysis_run_cht_rejects_contact_interface_mapping() {
    let _guard = analysis_test_guard();
    let mut model = sample_cht_model();
    set_model_contact(
        &mut model,
        ContactInterfaceOptions {
            enabled: true,
            penalty_stiffness_scale: 1.0,
            max_penetration_ratio: 0.01,
            friction_coefficient: 0.0,
        },
    );
    let err = analysis_run_cht_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("cht run should fail for contact interface mapping");

    assert_eq!(err.operation, "fea.run_cht");
    assert_eq!(err.op_version, "fea.run_cht/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CHT.INVALID_INTERFACE_MAPPING");
}

#[test]
fn analysis_run_cht_uses_authored_conjugate_heat_transfer_interface() {
    let _guard = analysis_test_guard();
    let mut model = sample_cht_model();
    model.interfaces = vec![runmat_analysis_core::AnalysisInterface {
        interface_id: "cht_channel_slab_interface".to_string(),
        primary_region_id: "fluid_channel".to_string(),
        secondary_region_id: "solid_slab".to_string(),
        kind: runmat_analysis_core::AnalysisInterfaceKind::ConjugateHeatTransfer(
            runmat_analysis_core::ConjugateHeatTransferInterfaceModel {
                thermal_conductance_w_per_m2k: 750.0,
                contact_resistance_m2k_per_w: 0.0,
                relaxation_factor: 0.5,
            },
        ),
    }];

    let envelope = analysis_run_cht_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisChtRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 4,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cht run should accept authored conjugate heat-transfer interface");

    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CHT_INTERFACE_CLOSURE"
            && diag.message.contains("interface_conductance_w_per_m2k=750")
    }));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CHT_COUPLING"
            && diag.message.contains("authored_interface_count=1")));
}

#[test]
fn analysis_run_fsi_rejects_models_without_transient_step() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "cfd_1".to_string(),
        kind: AnalysisStepKind::Cfd,
    }];
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::Transient, true));
    let err = analysis_run_fsi_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("fsi run should fail for missing transient step");

    assert_eq!(err.operation, "fea.run_fsi");
    assert_eq!(err.op_version, "fea.run_fsi/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_FSI.INVALID_MODEL");
}

#[test]
fn analysis_run_fsi_rejects_invalid_cfd_domain_parameters() {
    let _guard = analysis_test_guard();
    let mut model = sample_fsi_model();
    model
        .cfd
        .as_mut()
        .expect("fsi model should include cfd")
        .dynamic_viscosity_pa_s = 0.0;
    let err = analysis_run_fsi_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("fsi run should fail for invalid cfd domain values");

    assert_eq!(err.operation, "fea.run_fsi");
    assert_eq!(err.op_version, "fea.run_fsi/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_FSI.INVALID_OPTIONS");
}

#[test]
fn analysis_run_fsi_rejects_moment_loads() {
    let _guard = analysis_test_guard();
    let mut model = sample_fsi_model();
    model.loads = vec![sample_moment_load("fsi_moment")];

    let err = analysis_run_fsi_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("fsi run should reject structural moment loads");

    assert_eq!(err.operation, "fea.run_fsi");
    assert_eq!(err.op_version, "fea.run_fsi/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_FSI.INVALID_LOAD");
}

#[test]
fn analysis_run_fsi_rejects_contact_interface_mapping() {
    let _guard = analysis_test_guard();
    let mut model = sample_fsi_model();
    set_model_contact(
        &mut model,
        ContactInterfaceOptions {
            enabled: true,
            penalty_stiffness_scale: 1.0,
            max_penetration_ratio: 0.01,
            friction_coefficient: 0.0,
        },
    );
    let err = analysis_run_fsi_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("fsi run should fail for contact interface mapping");

    assert_eq!(err.operation, "fea.run_fsi");
    assert_eq!(err.op_version, "fea.run_fsi/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_FSI.INVALID_INTERFACE_MAPPING");
}

#[test]
fn analysis_run_fsi_uses_authored_fluid_structure_interface() {
    let _guard = analysis_test_guard();
    let mut model = sample_fsi_model();
    model.interfaces = vec![runmat_analysis_core::AnalysisInterface {
        interface_id: "fsi_pipe_plate_interface".to_string(),
        primary_region_id: "fluid_pipe".to_string(),
        secondary_region_id: "plate_wall".to_string(),
        kind: runmat_analysis_core::AnalysisInterfaceKind::FluidStructure(
            runmat_analysis_core::FluidStructureInterfaceModel {
                normal_stiffness_pa_per_m: 8.0e8,
                damping_ratio: 0.04,
                relaxation_factor: 0.5,
            },
        ),
    }];

    let envelope = analysis_run_fsi_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisFsiRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 4,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("fsi run should accept authored fluid-structure interface");

    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_FSI_INTERFACE_CLOSURE"
            && diag
                .message
                .contains("interface_stiffness_pa_per_m=800000000")
    }));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_FSI_COUPLING"
            && diag.message.contains("authored_interface_count=1")));
}

#[test]
fn analysis_run_electromagnetic_rejects_models_without_em_step() {
    let model = sample_model();
    let err = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-missing-step".to_string()), None),
    )
    .expect_err("electromagnetic run should fail without electromagnetic step");
    assert_eq!(err.operation, "fea.run_electromagnetic");
    assert_eq!(err.op_version, "fea.run_electromagnetic/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_ELECTROMAGNETIC.REQUIRES_STEP");
}

fn configure_valid_em_authoring(model: &mut AnalysisModel) {
    for material in &mut model.materials {
        if material.electrical.is_none() {
            material.electrical = Some(MaterialElectricalModel::default());
        }
    }
    model.electromagnetic = Some(ElectromagneticDomain {
        enabled: true,
        reference_frequency_hz: 60.0,
        applied_current_a: 120.0,
    });
    model.boundary_conditions = vec![
        BoundaryCondition {
            bc_id: "em_ground".to_string(),
            region_id: "em_region".to_string(),
            kind: BoundaryConditionKind::VectorPotentialGround,
        },
        BoundaryCondition {
            bc_id: "em_insulation".to_string(),
            region_id: "em_region".to_string(),
            kind: BoundaryConditionKind::MagneticInsulation,
        },
    ];
    model.loads = vec![LoadCase {
        load_id: "em_source".to_string(),
        region_id: "em_region".to_string(),
        kind: LoadKind::CoilCurrent {
            current_a: 120.0,
            phase_rad: 0.0,
            amplitude_scale: 1.0,
        },
    }];
}

#[test]
fn analysis_run_electromagnetic_rejects_missing_electrical_material() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    configure_valid_em_authoring(&mut model);
    for material in &mut model.materials {
        material.electrical = None;
    }

    let err = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-missing-material".to_string()), None),
    )
    .expect_err("electromagnetic run should reject missing electrical material");
    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ELECTROMAGNETIC.MISSING_ELECTROMAGNETIC_MATERIAL"
    );
}

#[test]
fn analysis_run_electromagnetic_rejects_missing_current_source() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    configure_valid_em_authoring(&mut model);
    model.loads = vec![LoadCase {
        load_id: "structural_force".to_string(),
        region_id: "em_region".to_string(),
        kind: LoadKind::Force {
            fx: 1.0,
            fy: 0.0,
            fz: 0.0,
        },
    }];

    let err = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-missing-source".to_string()), None),
    )
    .expect_err("electromagnetic run should reject missing current source");
    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ELECTROMAGNETIC.MISSING_ELECTROMAGNETIC_SOURCE"
    );
}

#[test]
fn analysis_run_electromagnetic_rejects_moment_loads() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    configure_valid_em_authoring(&mut model);
    model.loads = vec![sample_moment_load("em_moment")];

    let err = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-moment-load".to_string()), None),
    )
    .expect_err("electromagnetic run should reject structural moment loads");
    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ELECTROMAGNETIC.INVALID_ELECTROMAGNETIC_SOURCE"
    );
}

#[test]
fn analysis_run_electromagnetic_rejects_missing_em_boundary() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    configure_valid_em_authoring(&mut model);
    model.boundary_conditions = vec![BoundaryCondition {
        bc_id: "structural_fixed".to_string(),
        region_id: "em_region".to_string(),
        kind: BoundaryConditionKind::Fixed,
    }];

    let err = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-missing-boundary".to_string()), None),
    )
    .expect_err("electromagnetic run should reject missing EM boundary");
    assert_eq!(
        err.error_code,
        "RM.FEA.RUN_ELECTROMAGNETIC.MISSING_ELECTROMAGNETIC_BOUNDARY"
    );
}

#[test]
fn analysis_run_electromagnetic_static_contract_emits_typed_payload() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    model.materials[0].electrical = Some(MaterialElectricalModel {
        reference_temperature_k: 293.15,
        conductivity_s_per_m: 5.8e7,
        resistive_heating_coefficient: 0.0039,
        relative_permittivity: 3.2,
        relative_permeability: 1.8,
        conductivity_frequency_response: vec![
            ConductivityFrequencyPoint {
                frequency_hz: 40.0,
                conductivity_scale: 1.06,
                dispersive_loss_scale: Some(0.02),
                relative_permittivity_scale: Some(1.04),
                relative_permeability_scale: Some(1.02),
            },
            ConductivityFrequencyPoint {
                frequency_hz: 60.0,
                conductivity_scale: 1.0,
                dispersive_loss_scale: Some(0.03),
                relative_permittivity_scale: Some(1.0),
                relative_permeability_scale: Some(1.0),
            },
            ConductivityFrequencyPoint {
                frequency_hz: 240.0,
                conductivity_scale: 0.91,
                dispersive_loss_scale: Some(0.04),
                relative_permittivity_scale: Some(0.96),
                relative_permeability_scale: Some(0.98),
            },
        ],
    });
    model.electromagnetic = Some(ElectromagneticDomain {
        enabled: true,
        reference_frequency_hz: 60.0,
        applied_current_a: 120.0,
    });
    configure_valid_em_authoring(&mut model);
    let envelope = analysis_run_electromagnetic_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(Some("trace-em-run-static".to_string()), None),
    )
    .expect("electromagnetic run should return static EM payload");
    assert_eq!(envelope.operation, "fea.run_electromagnetic");
    assert_eq!(envelope.op_version, "fea.run_electromagnetic/v1");
    assert_ne!(envelope.data.run_status, RunStatus::Rejected);
    assert_eq!(
        envelope.data.run.solver_method,
        "electromagnetic_edge_curl_curl_harmonic_block_bicgstab"
    );
    assert!(envelope.data.electromagnetic_results.is_some());
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_EM_STATIC"));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_EM_HARMONIC_COUPLING"
            && diag.message.contains("formulation=edge_curl_curl_harmonic")
            && diag.message.contains("edge_dof_count=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_EM_MAXWELL_EDGE_TOPOLOGY"
            && diag.message.contains("incidence_element_count=")
            && diag.message.contains("incidence_orientation_count=")
            && diag
                .message
                .contains("incidence_operator_pair_coverage_ratio=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_EM_FORMULATION"
            && diag
                .message
                .contains("formulation_family=frequency_domain_maxwell")
            && diag
                .message
                .contains("active_formulation=full_wave_harmonic")
            && diag
                .message
                .contains("includes_magnetostatic_curl_curl=true")
            && diag
                .message
                .contains("includes_magnetoquasistatic_eddy_current=true")
            && diag
                .message
                .contains("includes_full_wave_displacement_current=true")
            && diag.message.contains("formulation_coverage_ratio=1")
            && diag
                .message
                .contains("material_frequency_response_coverage_ratio=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_EM_SOURCE_ENERGY"
            && diag.message.contains("source_region_coverage_ratio=")
            && diag
                .message
                .contains("source_region_energy_consistency_ratio=")
            && diag.message.contains("energy_imbalance_ratio=")
            && diag.message.contains("boundary_energy_ratio=")
    }));
    let em_cost_diag = envelope
        .data
        .run
        .diagnostics
        .iter()
        .find(|diag| diag.code == "FEA_EM_COST")
        .expect("EM cost diagnostic must be present");
    assert!(em_cost_diag.message.contains("prepared_build_ms="));
    assert!(em_cost_diag.message.contains("solve_ms="));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_EM_SWEEP"));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_EM_KNOWN_ANSWER"
            && diag.message.contains("basis=homogeneous_current_line")
            && diag
                .message
                .contains("homogeneous_material_residual_ratio=")
            && diag
                .message
                .contains("source_energy_consistency_residual_ratio=")
            && diag.message.contains("gauge_anchor_residual_ratio=")
            && diag.message.contains("known_answer_coverage_ratio=")
    }));
    let em_payload = envelope
        .data
        .electromagnetic_results
        .as_ref()
        .expect("electromagnetic payload expected");
    assert_eq!(em_payload.sweep_frequency_hz.len(), 1);
    assert_eq!(em_payload.sweep_peak_flux_density.len(), 1);
    assert_eq!(em_payload.sweep_solve_quality.len(), 1);
    assert!(em_payload.resonance_peak_frequency_hz.is_some());
    assert!(em_payload.resonance_peak_flux_density.is_some());
    assert_eq!(
        em_payload.vector_potential_real.field_id,
        FEA_FIELD_EM_VECTOR_POTENTIAL_REAL
    );
    assert_eq!(
        em_payload.vector_potential_imag.field_id,
        FEA_FIELD_EM_VECTOR_POTENTIAL_IMAG
    );
    assert_eq!(em_payload.vector_potential_real.shape.len(), 2);
    assert_eq!(em_payload.vector_potential_real.shape[1], 3);
    assert_eq!(
        em_payload.vector_potential_imag.shape,
        em_payload.vector_potential_real.shape
    );
    match &em_payload.vector_potential_real.values {
        AnalysisFieldValues::HostF64(values) => {
            assert_eq!(
                values.len(),
                em_payload
                    .vector_potential_real
                    .shape
                    .iter()
                    .product::<usize>()
            );
        }
        AnalysisFieldValues::DeviceRef(_) => {
            panic!("CPU EM vector potential should be returned as host values")
        }
    }
    assert_eq!(
        em_payload.magnetic_flux_density_real.field_id,
        FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_REAL
    );
    assert_eq!(
        em_payload.current_density_real.field_id,
        FEA_FIELD_EM_CURRENT_DENSITY_REAL
    );
    assert_eq!(
        em_payload.electric_field_real.field_id,
        FEA_FIELD_EM_ELECTRIC_FIELD_REAL
    );
    assert_eq!(
        em_payload.energy_density.field_id,
        FEA_FIELD_EM_ENERGY_DENSITY
    );
    assert_eq!(
        em_payload.residual_real.field_id,
        FEA_FIELD_EM_RESIDUAL_REAL
    );
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("em results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("EM descriptor should be present")
    };
    for field_id in [
        FEA_FIELD_EM_VECTOR_POTENTIAL_IMAG,
        FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_REAL,
        FEA_FIELD_EM_MAGNETIC_FIELD_REAL,
        FEA_FIELD_EM_CURRENT_DENSITY_REAL,
        FEA_FIELD_EM_ELECTRIC_FIELD_REAL,
        FEA_FIELD_EM_ELECTRIC_FLUX_DENSITY_REAL,
        FEA_FIELD_EM_POYNTING_VECTOR_REAL,
    ] {
        let descriptor = descriptor(field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
    assert_eq!(
        descriptor(FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_MAGNITUDE).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_MAGNITUDE).component_count,
        None
    );
    assert_eq!(
        descriptor(FEA_FIELD_EM_ENERGY_DENSITY).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(FEA_FIELD_EM_RESIDUAL_REAL).kind,
        AnalysisFieldKind::Scalar
    );
    let em_diag = envelope
        .data
        .run
        .diagnostics
        .iter()
        .find(|diag| diag.code == "FEA_EM_STATIC")
        .expect("EM static diagnostic must be present");
    assert!(em_diag.message.contains("relative_permittivity_mean="));
    assert!(em_diag.message.contains("relative_permeability_mean="));
    assert!(em_diag
        .message
        .contains("conductivity_frequency_scale_mean="));
    assert!(em_diag
        .message
        .contains("conductivity_frequency_response_coverage_ratio="));
    assert!(em_diag
        .message
        .contains("relative_permittivity_frequency_scale_mean="));
    assert!(em_diag
        .message
        .contains("relative_permittivity_frequency_response_coverage_ratio="));
    assert!(em_diag
        .message
        .contains("relative_permeability_frequency_scale_mean="));
    assert!(em_diag
        .message
        .contains("relative_permeability_frequency_response_coverage_ratio="));
    assert!(em_diag.message.contains("dispersive_loss_scale_mean="));
    assert!(em_diag
        .message
        .contains("dispersive_phase_attenuation_mean="));
    assert!(em_diag
        .message
        .contains("dispersive_phase_conductivity_attenuation_ratio="));
}

#[test]
fn analysis_run_electromagnetic_sweep_emits_resonance_metrics() {
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    model.materials[0].electrical = Some(MaterialElectricalModel {
        reference_temperature_k: 293.15,
        conductivity_s_per_m: 5.8e7,
        resistive_heating_coefficient: 0.0039,
        relative_permittivity: 3.2,
        relative_permeability: 1.8,
        conductivity_frequency_response: vec![
            ConductivityFrequencyPoint {
                frequency_hz: 40.0,
                conductivity_scale: 1.06,
                dispersive_loss_scale: Some(0.02),
                relative_permittivity_scale: Some(1.04),
                relative_permeability_scale: Some(1.02),
            },
            ConductivityFrequencyPoint {
                frequency_hz: 60.0,
                conductivity_scale: 1.0,
                dispersive_loss_scale: Some(0.03),
                relative_permittivity_scale: Some(1.0),
                relative_permeability_scale: Some(1.0),
            },
            ConductivityFrequencyPoint {
                frequency_hz: 240.0,
                conductivity_scale: 0.91,
                dispersive_loss_scale: Some(0.04),
                relative_permittivity_scale: Some(0.96),
                relative_permeability_scale: Some(0.98),
            },
        ],
    });
    model.electromagnetic = Some(ElectromagneticDomain {
        enabled: true,
        reference_frequency_hz: 60.0,
        applied_current_a: 120.0,
    });
    configure_valid_em_authoring(&mut model);
    let envelope = analysis_run_electromagnetic_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisElectromagneticRunOptions {
            sweep_enabled: true,
            sweep_frequency_hz: vec![20.0, 40.0, 60.0, 120.0, 240.0],
            residual_target: 5.0e-7,
            harmonic_tolerance: 1.2345e-4,
            harmonic_max_iterations: 64,
            ..AnalysisElectromagneticRunOptions::default()
        },
        OperationContext::new(Some("trace-em-run-sweep".to_string()), None),
    )
    .expect("electromagnetic sweep run should succeed");
    let payload = envelope
        .data
        .electromagnetic_results
        .as_ref()
        .expect("electromagnetic payload expected");
    assert_eq!(payload.sweep_frequency_hz.len(), 5);
    assert_eq!(payload.sweep_peak_flux_density.len(), 5);
    assert_eq!(payload.sweep_solve_quality.len(), 5);
    assert!(payload.resonance_peak_frequency_hz.is_some());
    assert!(payload.resonance_peak_flux_density.is_some());
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .filter(|diag| diag.code == "FEA_EM_SWEEP")
        .any(|diag| diag.message.contains("sweep_count=5")));
    let harmonic_diag = envelope
        .data
        .run
        .diagnostics
        .iter()
        .find(|diag| diag.code == "FEA_EM_HARMONIC_COUPLING")
        .expect("harmonic coupling diagnostic should be present");
    assert!(harmonic_diag.message.contains("tolerance=0.00012345"));
    assert!(harmonic_diag.message.contains("iterations="));
}

#[test]
fn analysis_run_electromagnetic_rejects_invalid_harmonic_controls() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Electromagnetic;
    model.electromagnetic = Some(ElectromagneticDomain {
        enabled: true,
        reference_frequency_hz: 60.0,
        applied_current_a: 120.0,
    });
    let err = analysis_run_electromagnetic_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisElectromagneticRunOptions {
            harmonic_max_iterations: 0,
            ..AnalysisElectromagneticRunOptions::default()
        },
        OperationContext::new(
            Some("trace-em-run-invalid-harmonic-controls".to_string()),
            None,
        ),
    )
    .expect_err("electromagnetic run should reject zero harmonic_max_iterations");
    assert_eq!(err.operation, "fea.run_electromagnetic");
    assert_eq!(err.op_version, "fea.run_electromagnetic/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_ELECTROMAGNETIC.INVALID_OPTIONS");
}

#[test]
fn analysis_run_thermal_returns_temperature_payload() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_1".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 60.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: vec![ThermoRegionTemperatureDelta {
                region_id: "tip".to_string(),
                temperature_delta_k: 70.0,
            }],
            time_profile: vec![
                ThermoTimeProfilePoint {
                    normalized_time: 0.0,
                    scale: 0.5,
                },
                ThermoTimeProfilePoint {
                    normalized_time: 1.0,
                    scale: 1.0,
                },
            ],
        },
    );
    let run = analysis_run_thermal_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisThermalRunOptions {
            step_count: 6,
            ..AnalysisThermalRunOptions::default()
        },
        OperationContext::new(None, None),
    )
    .expect("thermal run should succeed");

    assert_eq!(run.operation, "fea.run_thermal");
    assert_eq!(run.op_version, "fea.run_thermal/v1");
    assert!(run.data.thermal_results.is_some());
    assert!(run.data.transient_results.is_none());
    assert!(run.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_THERMAL_HEAT_BALANCE"
            && diag.message.contains("input_heat=")
            && diag.message.contains("boundary_heat=")
            && diag.message.contains("stored_energy=")
            && diag.message.contains("numerical_loss=")
            && diag.message.contains("heat_balance_residual_ratio=")
    }));
    assert!(run.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_THERMAL_FIELD_RECOVERY"
            && diag.message.contains("recovery_dimensions=")
            && diag.message.contains("boundary_face_count=6")
    }));
    assert!(run.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_THERMAL_KNOWN_ANSWER"
            && diag.message.contains("slab_linear_profile_rms_ratio=")
            && diag.message.contains("slab_monotonic_edge_fraction=")
            && diag.message.contains("lumped_response_error_ratio=")
            && diag.message.contains("source_response_sign_alignment=")
    }));
    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("fea.results should return thermal payload");
    let thermal = results
        .data
        .thermal_results
        .as_ref()
        .expect("thermal results must be present");
    assert_eq!(thermal.time_points_s.len(), 6);
    assert_eq!(thermal.temperature_snapshots.len(), 6);
    assert_eq!(thermal.temperature_gradient_snapshots.len(), 6);
    assert_eq!(thermal.heat_flux_snapshots.len(), 6);
    assert_eq!(thermal.heat_source_snapshots.len(), 6);
    assert_eq!(thermal.boundary_heat_flux_snapshots.len(), 6);
    assert_eq!(thermal.temperature_gradient_snapshots[0].shape.len(), 2);
    assert_eq!(thermal.temperature_gradient_snapshots[0].shape[1], 3);
    assert_eq!(
        thermal.heat_flux_snapshots[0].shape,
        thermal.temperature_gradient_snapshots[0].shape
    );
    assert_eq!(
        thermal.heat_source_snapshots[0].shape,
        vec![thermal.temperature_gradient_snapshots[0].shape[0]]
    );
    assert_eq!(thermal.boundary_heat_flux_snapshots[0].shape, vec![6]);
    let field_ids = results
        .data
        .field_descriptors
        .iter()
        .map(|descriptor| descriptor.field_id.as_str())
        .collect::<Vec<_>>();
    assert!(field_ids.contains(&fea_thermal_temperature_gradient_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_thermal_heat_flux_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_thermal_heat_source_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_thermal_boundary_heat_flux_field_id(0).as_str()));
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("thermal field descriptor should be present")
    };
    assert_eq!(
        descriptor(&fea_thermal_heat_flux_field_id(0)).kind,
        AnalysisFieldKind::Vector
    );
    assert_eq!(
        descriptor(&fea_thermal_heat_flux_field_id(0)).component_count,
        Some(3)
    );
    assert_eq!(
        descriptor(&fea_thermal_boundary_heat_flux_field_id(0)).kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(
        descriptor(&fea_thermal_boundary_heat_flux_field_id(0)).component_count,
        None
    );
    assert_eq!(results.data.summary.snapshot_count, 6);
}

#[test]
fn analysis_run_thermal_balanced_degrades_on_high_constitutive_spread() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "thermal_1".to_string(),
        kind: AnalysisStepKind::Thermal,
    }];
    model.materials.push(MaterialModel {
        material_id: "mat_poly_high_k".to_string(),
        name: "High K Composite".to_string(),
        mechanical: MaterialMechanicalModel {
            youngs_modulus_pa: 5.0e9,
            poisson_ratio: 0.33,
            density_kg_per_m3: 1800.0,
        },
        thermal: MaterialThermalModel {
            reference_temperature_k: 293.15,
            conductivity_w_per_mk: 1200.0,
            specific_heat_j_per_kgk: 160.0,
            ..MaterialThermalModel::default()
        },
        acoustic: None,
        electrical: None,
        plastic: None,
    });
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 80.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: vec![
                ThermoRegionTemperatureDelta {
                    region_id: "tip".to_string(),
                    temperature_delta_k: 95.0,
                },
                ThermoRegionTemperatureDelta {
                    region_id: "root".to_string(),
                    temperature_delta_k: 60.0,
                },
            ],
            time_profile: vec![
                ThermoTimeProfilePoint {
                    normalized_time: 0.0,
                    scale: 0.3,
                },
                ThermoTimeProfilePoint {
                    normalized_time: 1.0,
                    scale: 1.0,
                },
            ],
        },
    );

    let run = analysis_run_thermal_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisThermalRunOptions {
            quality_policy: QualityPolicy::Balanced,
            step_count: 8,
            ..AnalysisThermalRunOptions::default()
        },
        OperationContext::new(None, None),
    )
    .expect("thermal run should execute");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::ThermalConstitutiveSpreadHigh));
}

#[test]
fn analysis_run_nonlinear_rejects_models_without_nonlinear_step() {
    let _guard = analysis_test_guard();
    let model = sample_model();
    let err = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("nonlinear run should reject models without nonlinear step");
    assert_eq!(err.operation, "fea.run_nonlinear");
    assert_eq!(err.op_version, "fea.run_nonlinear/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_NONLINEAR.INVALID_MODEL");
}

#[test]
fn analysis_run_nonlinear_returns_native_nonlinear_result() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let envelope = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            increment_count: 16,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");

    assert_eq!(envelope.operation, "fea.run_nonlinear");
    assert_eq!(envelope.op_version, "fea.run_nonlinear/v1");
    let nonlinear = envelope
        .data
        .nonlinear_results
        .as_ref()
        .expect("nonlinear payload should exist");
    assert_eq!(nonlinear.method, NonlinearMethod::IncrementalNewtonRaphson);
    assert_eq!(nonlinear.load_factors.len(), 16);
    assert_eq!(nonlinear.load_factors.len(), nonlinear.residual_norms.len());
    assert_eq!(
        nonlinear.residual_norms.len(),
        nonlinear.increment_norms.len()
    );
    assert_eq!(
        nonlinear.residual_norms.len(),
        nonlinear.iteration_counts.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.von_mises_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.plastic_strain_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.equivalent_plastic_strain_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.contact_pressure_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.contact_gap_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.load_factor_snapshots.len()
    );
    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.residual_norm_snapshots.len()
    );
    assert_eq!(
        nonlinear.von_mises_snapshots[0].field_id,
        fea_nonlinear_von_mises_field_id(0)
    );
    assert_eq!(
        nonlinear.plastic_strain_snapshots[0].field_id,
        fea_nonlinear_plastic_strain_field_id(0)
    );
    assert_eq!(
        nonlinear.equivalent_plastic_strain_snapshots[0].field_id,
        fea_nonlinear_equivalent_plastic_strain_field_id(0)
    );
    assert_eq!(
        nonlinear.contact_pressure_snapshots[0].field_id,
        fea_nonlinear_contact_pressure_field_id(0)
    );
    assert_eq!(
        nonlinear.contact_gap_snapshots[0].field_id,
        fea_nonlinear_contact_gap_field_id(0)
    );
    assert_eq!(
        nonlinear.load_factor_snapshots[0].field_id,
        fea_nonlinear_load_factor_field_id(0)
    );
    assert_eq!(
        nonlinear.residual_norm_snapshots[0].field_id,
        fea_nonlinear_residual_norm_field_id(0)
    );
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("nonlinear results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("nonlinear descriptor should be present")
    };
    for field_id in [
        fea_nonlinear_von_mises_field_id(0),
        fea_nonlinear_equivalent_plastic_strain_field_id(0),
        fea_nonlinear_contact_pressure_field_id(0),
        fea_nonlinear_contact_gap_field_id(0),
        fea_nonlinear_load_factor_field_id(0),
        fea_nonlinear_residual_norm_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    let plastic_strain_descriptor = descriptor(&fea_nonlinear_plastic_strain_field_id(0));
    assert_eq!(plastic_strain_descriptor.kind, AnalysisFieldKind::Tensor);
    assert_eq!(plastic_strain_descriptor.component_count, Some(6));
    assert!(nonlinear.tangent_rebuild_count > 0);
    assert!(nonlinear.iteration_spike_count <= nonlinear.load_factors.len());
    assert!(nonlinear.max_line_search_backtracks_per_increment > 0);
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_NONLINEAR_CONVERGENCE"));
}

#[test]
fn analysis_run_nonlinear_exposes_beam_rotation_snapshots() {
    let _guard = analysis_test_guard();
    let mut model = runmat_analysis_fea::fixtures::fixture_model(
        runmat_analysis_fea::fixtures::FixtureId::StructuralBeamCantileverEndMomentReference,
    );
    model.steps = vec![AnalysisStep {
        step_id: "beam_moment_nonlinear".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];

    let envelope = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            increment_count: 8,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("beam nonlinear run should succeed");
    let nonlinear = envelope
        .data
        .nonlinear_results
        .as_ref()
        .expect("nonlinear payload should exist");

    assert_eq!(
        nonlinear.load_factors.len(),
        nonlinear.rotation_snapshots.len()
    );
    assert_eq!(
        nonlinear.rotation_snapshots[0].field_id,
        fea_nonlinear_rotation_field_id(0)
    );
    assert_eq!(nonlinear.rotation_snapshots[0].shape, vec![2, 3]);

    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("nonlinear beam results should be queryable");
    let descriptor = results
        .data
        .field_descriptors
        .iter()
        .find(|descriptor| descriptor.field_id == fea_nonlinear_rotation_field_id(0))
        .expect("nonlinear rotation descriptor should be present");
    assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
    assert_eq!(descriptor.component_count, Some(3));
}

#[test]
fn analysis_run_nonlinear_strict_rejects_iteration_cap_exhaustion() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];

    let envelope = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            quality_policy: QualityPolicy::Strict,
            max_newton_iters: 1,
            line_search: false,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should produce envelope");

    assert_eq!(envelope.data.run_status, RunStatus::Degraded);
    assert!(!envelope.data.publishable);
    assert!(envelope
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::NonlinearIncrementFailure));
}

#[test]
fn analysis_run_nonlinear_rejects_missing_prep_artifact_reference() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];

    let error = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            prep_artifact_id: Some("prep:missing".to_string()),
            ..AnalysisNonlinearRunOptions::production_recommended()
        },
        OperationContext::new(None, None),
    )
    .expect_err("missing prep artifact reference should fail");
    assert_eq!(error.error_code, "RM.FEA.RUN_PREP.NOT_FOUND");
}

#[test]
fn analysis_run_nonlinear_rejects_mismatched_prep_artifact_reference() {
    let _guard = analysis_test_guard();
    let _prep_guard = crate::geometry::prep_artifact_test_guard();
    let geometry = sample_step_like_geometry_asset();
    let prep = crate::geometry::geometry_prep_for_analysis_op(
        &geometry,
        crate::geometry::GeometryPrepForAnalysisSpec::default(),
        OperationContext::new(None, None),
    )
    .expect("prep should succeed");

    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];

    let error = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            prep_artifact_id: Some(prep.data.prep_artifact_id.clone()),
            ..AnalysisNonlinearRunOptions::production_recommended()
        },
        OperationContext::new(None, None),
    )
    .expect_err("mismatched prep artifact reference should fail");
    assert_eq!(error.error_code, "RM.FEA.RUN_PREP.MISMATCH");
}

#[test]
fn analysis_run_nonlinear_rejects_stale_prep_artifact_when_newer_revision_exists() {
    let _guard = analysis_test_guard();
    let _prep_guard = crate::geometry::prep_artifact_test_guard();
    crate::geometry::reset_prep_artifact_store_for_tests();
    crate::geometry::configure_prep_artifacts(crate::geometry::GeometryPrepArtifactConfig {
        require_latest_revision: Some(true),
        ..crate::geometry::GeometryPrepArtifactConfig::default()
    })
    .expect("prep artifact config should be configurable");

    let mut geometry_v1 = sample_step_like_geometry_asset();
    geometry_v1.revision = 1;
    let mut geometry_v2 = geometry_v1.clone();
    geometry_v2.revision = 2;

    let prep_v1 = crate::geometry::geometry_prep_for_analysis_op(
        &geometry_v1,
        crate::geometry::GeometryPrepForAnalysisSpec::default(),
        OperationContext::new(None, None),
    )
    .expect("prep v1 should succeed");
    let _prep_v2 = crate::geometry::geometry_prep_for_analysis_op(
        &geometry_v2,
        crate::geometry::GeometryPrepForAnalysisSpec::default(),
        OperationContext::new(None, None),
    )
    .expect("prep v2 should succeed");

    let created = analysis_create_model_op(
        &geometry_v1,
        AnalysisCreateModelIntentSpec {
            model_id: "stale_prep_model".to_string(),
            profile: AnalysisCreateModelProfile::NonlinearStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("create model should succeed");

    let error = analysis_run_nonlinear_with_options_op(
        &created.data,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            prep_artifact_id: Some(prep_v1.data.prep_artifact_id),
            ..AnalysisNonlinearRunOptions::production_recommended()
        },
        OperationContext::new(None, None),
    )
    .expect_err("stale prep artifact should fail");
    assert_eq!(error.error_code, "RM.FEA.RUN_PREP.STALE");

    let health = crate::geometry::geometry_prep_artifact_health_op(
        crate::geometry::GeometryPrepArtifactHealthQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("prep health should be queryable");
    assert!(health.data.metrics.stale_reject_count >= 1);

    crate::geometry::reset_prep_artifact_store_for_tests();
}

#[test]
fn nonlinear_quality_policy_diverges_for_increment_failures() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];

    let run_with_policy = |quality_policy| {
        analysis_run_nonlinear_with_options_op(
            &model,
            ComputeBackend::Cpu,
            AnalysisNonlinearRunOptions {
                quality_policy,
                max_newton_iters: 1,
                line_search: false,
                max_line_search_backtracks: 0,
                ..AnalysisNonlinearRunOptions::balanced()
            },
            OperationContext::new(None, None),
        )
        .expect("nonlinear run should return envelope")
    };

    let exploratory = run_with_policy(QualityPolicy::Exploratory);
    let balanced = run_with_policy(QualityPolicy::Balanced);
    let strict = run_with_policy(QualityPolicy::Strict);

    assert!(exploratory.data.publishable);
    assert_eq!(exploratory.data.run_status, RunStatus::Publishable);
    assert!(balanced
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::NonlinearIncrementFailure));
    assert!(!balanced.data.publishable);
    assert_eq!(balanced.data.run_status, RunStatus::Degraded);

    assert!(strict
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::NonlinearIncrementFailure));
    assert!(!strict.data.publishable);
    assert_eq!(strict.data.run_status, RunStatus::Degraded);
}

#[test]
fn nonlinear_balanced_degrades_when_thermo_mechanical_severity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    for material in &mut model.materials {
        material.thermal.expansion_coefficient_per_k = 1.0e-3;
    }
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.0e-3,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            quality_policy: QualityPolicy::Balanced,
            ..AnalysisNonlinearRunOptions::production_recommended()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::ThermoMechanicalNonlinearStress));

    let nonlinear = run
        .data
        .nonlinear_results
        .as_ref()
        .expect("nonlinear results should be present");
    assert_eq!(
        nonlinear.thermo_mechanical_temperature_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.thermo_mechanical_thermal_strain_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.thermo_mechanical_thermal_stress_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.thermo_mechanical_displacement_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.thermo_mechanical_von_mises_snapshots.len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear
            .thermo_mechanical_coupling_residual_snapshots
            .len(),
        nonlinear.load_factors.len()
    );
    assert_eq!(
        nonlinear.thermo_mechanical_temperature_snapshots[0].field_id,
        fea_thermo_mechanical_temperature_field_id(0)
    );
    assert_eq!(
        nonlinear.thermo_mechanical_thermal_strain_snapshots[0].field_id,
        fea_thermo_mechanical_thermal_strain_field_id(0)
    );
    assert_eq!(
        nonlinear.thermo_mechanical_thermal_stress_snapshots[0].field_id,
        fea_thermo_mechanical_thermal_stress_field_id(0)
    );
    assert_eq!(
        nonlinear.thermo_mechanical_displacement_snapshots[0].field_id,
        fea_thermo_mechanical_displacement_field_id(0)
    );
    assert_eq!(
        nonlinear.thermo_mechanical_von_mises_snapshots[0].field_id,
        fea_thermo_mechanical_von_mises_field_id(0)
    );
    assert_eq!(
        nonlinear.thermo_mechanical_coupling_residual_snapshots[0].field_id,
        fea_thermo_mechanical_coupling_residual_field_id(0)
    );

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("thermo-mechanical nonlinear results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("nonlinear thermo-mechanical descriptor should be present")
    };
    for field_id in [
        fea_thermo_mechanical_temperature_field_id(0),
        fea_thermo_mechanical_von_mises_field_id(0),
        fea_thermo_mechanical_coupling_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    let displacement_descriptor = descriptor(&fea_thermo_mechanical_displacement_field_id(0));
    assert_eq!(displacement_descriptor.kind, AnalysisFieldKind::Vector);
    assert_eq!(displacement_descriptor.component_count, Some(3));
    for field_id in [
        fea_thermo_mechanical_thermal_strain_field_id(0),
        fea_thermo_mechanical_thermal_stress_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Tensor);
        assert_eq!(descriptor.component_count, Some(6));
    }
}

#[test]
fn nonlinear_balanced_degrades_when_thermo_heterogeneity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model_with_material_assignment_mismatch();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            quality_policy: QualityPolicy::Balanced,
            ..AnalysisNonlinearRunOptions::production_recommended()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run.data.quality_reasons.iter().any(|reason| {
        reason.code == QualityReasonCode::ThermoMechanicalConstitutiveSpreadHigh
            || reason.code == QualityReasonCode::ThermoMechanicalAssignmentHeterogeneityHigh
    }));
}

#[test]
fn analysis_results_query_can_exclude_nonlinear_payload() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    let run = analysis_run_nonlinear_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: Vec::new(),
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: false,
            include_electromagnetic_results: false,
        },
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    assert!(results.data.nonlinear_results.is_none());
    assert!(results.data.summary.increment_count > 0);
    assert!(results.data.summary.failed_increment_count.is_some());
    assert!(results.data.summary.max_nonlinear_residual_norm.is_some());
    assert!(results.data.summary.max_nonlinear_increment_norm.is_some());
    assert!(results.data.summary.max_nonlinear_iteration_count.is_some());
    assert!(results.data.summary.final_increment_converged.is_some());
    assert!(results
        .data
        .summary
        .nonlinear_line_search_backtracks
        .is_some());
    assert!(results
        .data
        .summary
        .nonlinear_max_backtracks_per_increment
        .is_some());
    assert!(results
        .data
        .summary
        .nonlinear_tangent_rebuild_count
        .is_some());
    assert!(results
        .data
        .summary
        .nonlinear_iteration_spike_count
        .is_some());
    assert!(results
        .data
        .summary
        .nonlinear_convergence_stall_count
        .is_some());
    assert!(results
        .data
        .summary
        .nonlinear_backtrack_burst_count
        .is_some());
    let field_ids = results
        .data
        .field_descriptors
        .iter()
        .map(|descriptor| descriptor.field_id.as_str())
        .collect::<Vec<_>>();
    assert!(field_ids.contains(&fea_nonlinear_von_mises_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_nonlinear_plastic_strain_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_nonlinear_equivalent_plastic_strain_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_nonlinear_contact_pressure_field_id(0).as_str()));
    assert!(field_ids.contains(&fea_nonlinear_contact_gap_field_id(0).as_str()));
}

#[test]
fn nonlinear_results_deserialize_with_missing_new_fields() {
    let payload = serde_json::json!({
        "nonlinear_payload_version": "nonlinear_results/v1",
        "load_factors": [0.5, 1.0],
        "displacement_snapshots": [],
        "residual_norms": [1.0e-6, 5.0e-7],
        "method": "incremental_newton_raphson"
    });
    let parsed: NonlinearResultsData =
        serde_json::from_value(payload).expect("legacy nonlinear payload should deserialize");

    assert_eq!(parsed.increment_norms.len(), 0);
    assert_eq!(parsed.iteration_counts.len(), 0);
    assert_eq!(parsed.failed_increments, 0);
    assert_eq!(parsed.line_search_backtracks, 0);
    assert_eq!(parsed.max_line_search_backtracks_per_increment, 0);
    assert_eq!(parsed.tangent_rebuild_count, 0);
    assert_eq!(parsed.iteration_spike_count, 0);
    assert_eq!(parsed.convergence_stall_count, 0);
    assert_eq!(parsed.backtrack_burst_count, 0);
    assert!(parsed.von_mises_snapshots.is_empty());
    assert!(parsed.plastic_strain_snapshots.is_empty());
    assert!(parsed.equivalent_plastic_strain_snapshots.is_empty());
    assert!(parsed.contact_pressure_snapshots.is_empty());
    assert!(parsed.contact_gap_snapshots.is_empty());
}

#[test]
fn analysis_run_transient_returns_native_transient_result() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let envelope = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("transient run should return envelope");

    assert_eq!(envelope.operation, "fea.run_transient");
    assert_eq!(envelope.op_version, "fea.run_transient/v1");
    assert_eq!(envelope.data.run.solver_method, "implicit_euler_pcg");
    assert_eq!(envelope.data.provenance.solver_method, "implicit_euler_pcg");
    assert_eq!(envelope.data.run_status, RunStatus::Publishable);
    assert!(envelope.data.publishable);
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_TRANSIENT_CONVERGENCE"));
    let transient = envelope
        .data
        .transient_results
        .as_ref()
        .expect("transient payload should exist");
    assert_eq!(
        transient.integration_method,
        TransientIntegrationMethod::ImplicitEuler
    );
    assert!(!transient.time_points_s.is_empty());
    assert_eq!(
        transient.time_points_s.len(),
        transient.displacement_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.velocity_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.acceleration_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.von_mises_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.kinetic_energy_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.strain_energy_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.residual_norm_snapshots.len()
    );
    assert_eq!(
        transient.velocity_snapshots[1].field_id,
        fea_transient_velocity_field_id(1)
    );
    assert_eq!(
        transient.acceleration_snapshots[1].field_id,
        fea_transient_acceleration_field_id(1)
    );
    assert_eq!(
        transient.von_mises_snapshots[1].field_id,
        fea_transient_von_mises_field_id(1)
    );
    assert_eq!(
        transient.kinetic_energy_snapshots[1].field_id,
        fea_transient_kinetic_energy_field_id(1)
    );
    assert_eq!(
        transient.strain_energy_snapshots[1].field_id,
        fea_transient_strain_energy_field_id(1)
    );
    assert_eq!(
        transient.residual_norm_snapshots[1].field_id,
        fea_transient_residual_norm_field_id(1)
    );
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("transient results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("transient descriptor should be present")
    };
    for field_id in [
        fea_transient_velocity_field_id(1),
        fea_transient_acceleration_field_id(1),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
    for field_id in [
        fea_transient_von_mises_field_id(1),
        fea_transient_kinetic_energy_field_id(1),
        fea_transient_strain_energy_field_id(1),
        fea_transient_residual_norm_field_id(1),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
}

#[test]
fn analysis_run_transient_exposes_beam_rotational_snapshots() {
    let _guard = analysis_test_guard();
    let mut model = runmat_analysis_fea::fixtures::fixture_model(
        runmat_analysis_fea::fixtures::FixtureId::StructuralBeamCantileverEndMomentReference,
    );
    model.steps = vec![AnalysisStep {
        step_id: "beam_moment_transient".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let envelope = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("beam transient run should return envelope");
    let transient = envelope
        .data
        .transient_results
        .as_ref()
        .expect("transient payload should exist");

    assert_eq!(
        transient.time_points_s.len(),
        transient.rotation_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.angular_velocity_snapshots.len()
    );
    assert_eq!(
        transient.time_points_s.len(),
        transient.angular_acceleration_snapshots.len()
    );
    assert_eq!(
        transient.rotation_snapshots[1].field_id,
        fea_transient_rotation_field_id(1)
    );
    assert_eq!(
        transient.angular_velocity_snapshots[1].field_id,
        fea_transient_angular_velocity_field_id(1)
    );
    assert_eq!(
        transient.angular_acceleration_snapshots[1].field_id,
        fea_transient_angular_acceleration_field_id(1)
    );

    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("transient beam results should be queryable");
    for field_id in [
        fea_transient_rotation_field_id(1),
        fea_transient_angular_velocity_field_id(1),
        fea_transient_angular_acceleration_field_id(1),
    ] {
        let descriptor = results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("rotational transient descriptor should be present");
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
}

#[test]
fn analysis_run_cfd_returns_typed_payload_and_flow_diagnostics() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    model.boundary_conditions = sample_cfd_boundary_conditions(4.25);
    model
        .boundary_conditions
        .retain(|boundary| boundary.bc_id != "bc_cfd_wall_lower");
    if let Some(wall) = model
        .boundary_conditions
        .iter_mut()
        .find(|boundary| boundary.bc_id == "bc_cfd_wall_upper")
    {
        wall.kind = BoundaryConditionKind::CfdSlipWall;
    }
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));

    let envelope = analysis_run_cfd_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisCfdRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 4,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cfd run should return envelope");

    assert_eq!(envelope.operation, "fea.run_cfd");
    assert_eq!(envelope.op_version, "fea.run_cfd/v1");
    assert_eq!(
        envelope.data.run.solver_method,
        "cfd_velocity_pressure_finite_volume"
    );
    assert_eq!(
        envelope.data.provenance.solver_method,
        "cfd_velocity_pressure_finite_volume"
    );
    assert!(envelope.data.transient_results.is_none());
    let velocity = envelope
        .data
        .run
        .field(FEA_FIELD_CFD_VELOCITY)
        .expect("cfd velocity field should be present");
    let pressure = envelope
        .data
        .run
        .field(FEA_FIELD_CFD_PRESSURE)
        .expect("cfd pressure field should be present");
    let vorticity = envelope
        .data
        .run
        .field(FEA_FIELD_CFD_VORTICITY)
        .expect("cfd vorticity field should be present");
    let wall_shear = envelope
        .data
        .run
        .field(FEA_FIELD_CFD_WALL_SHEAR_STRESS)
        .expect("cfd wall-shear field should be present");
    assert_eq!(velocity.shape.len(), 2);
    assert_eq!(velocity.shape[1], 3);
    assert_eq!(pressure.shape, vec![velocity.shape[0]]);
    assert_eq!(vorticity.shape, velocity.shape);
    assert_eq!(wall_shear.shape, vec![1, 3]);
    assert_ne!(wall_shear.shape[0], velocity.shape[0]);
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CFD_FLOW"
            && diag.message.contains("inlet_velocity=4.25")
            && diag.message.contains("reynolds_number=")
            && diag.message.contains("solve_family=steady_state")
            && diag.message.contains("topology_basis=implicit_channel")));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_RESIDUAL"
            && diag.message.contains("max_momentum_residual=")
            && diag.message.contains("max_continuity_residual=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_ASSEMBLY"
            && diag
                .message
                .contains("basis=finite_volume_velocity_pressure")
            && diag.message.contains("topology_basis=implicit_channel")
            && diag
                .message
                .contains("topology_geometry_source=implicit_channel")
            && diag.message.contains("domain_length_m=")
            && diag.message.contains("face_area_m2=")
            && diag.message.contains("control_volume_volume_m3=")
            && diag.message.contains("nominal_mass_flow_rate_kg_per_s=")
            && diag.message.contains("courant_number=")
            && diag.message.contains("mass_balance_residual=")
            && diag.message.contains("pressure_drop_pa=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_BOUNDARY_CONDITIONS"
            && diag.message.contains("boundary_source=authored")
            && diag.message.contains("authored_boundary_count=3")
            && diag.message.contains("inlet_boundary_count=1")
            && diag.message.contains("outlet_boundary_count=1")
            && diag.message.contains("wall_boundary_count=1")
            && diag.message.contains("no_slip_wall_boundary_count=0")
            && diag.message.contains("slip_wall_boundary_count=1")
            && diag.message.contains("boundary_coverage_ratio=1")
            && diag.message.contains("nominal_inlet_velocity_m_per_s=4.25")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_PRESSURE_CORRECTION"
            && diag.message.contains("iteration_count=")
            && diag.message.contains("max_linear_iters=64")
            && diag.message.contains("tolerance=0.00000001")
            && diag.message.contains("pressure_correction_residual_ratio=")
            && diag.message.contains("velocity_correction_residual_ratio=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_TRANSIENT_EVOLUTION"
            && diag.message.contains("solve_family=steady_state")
            && diag.message.contains("transient_scale_variation=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_KNOWN_ANSWER"
            && diag.message.contains("pressure_drop_balance_ratio=")
            && diag.message.contains("mass_flux_uniformity_ratio=")
            && diag.message.contains("pressure_monotonic_cell_fraction=")
            && diag.message.contains("known_answer_coverage_ratio=")
    }));
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("cfd results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("CFD descriptor should be present")
    };
    for field_id in [
        FEA_FIELD_CFD_VELOCITY,
        FEA_FIELD_CFD_VORTICITY,
        FEA_FIELD_CFD_WALL_SHEAR_STRESS,
    ] {
        let descriptor = descriptor(field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
    for field_id in [
        FEA_FIELD_CFD_PRESSURE,
        FEA_FIELD_CFD_RESIDUAL_MOMENTUM,
        FEA_FIELD_CFD_RESIDUAL_CONTINUITY,
        FEA_FIELD_CFD_REYNOLDS_NUMBER,
    ] {
        let descriptor = descriptor(field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
}

#[test]
fn analysis_run_cfd_uses_prep_control_volume_topology() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    model.boundary_conditions = sample_cfd_boundary_conditions(3.0);
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));

    let run = solve_cfd_finite_volume_run(
        &model,
        model.cfd.as_ref().expect("cfd domain should exist"),
        ComputeBackend::Cpu,
        &AnalysisCfdRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 2,
            max_linear_iters: 32,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: Some(sample_analysis_run_prep_context()),
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        Some(&sample_analysis_run_prep_context()),
    );

    let velocity = run
        .field(FEA_FIELD_CFD_VELOCITY)
        .expect("cfd velocity field should be present");
    assert_eq!(velocity.shape, vec![15, 3]);
    assert!(run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_ASSEMBLY"
            && diag
                .message
                .contains("topology_basis=prep_control_volume_connectivity")
            && diag
                .message
                .contains("topology_geometry_source=prep_element_geometry")
            && diag.message.contains("control_volume_count=15")
            && diag.message.contains("control_volume_face_count=19")
            && diag
                .message
                .contains("control_volume_internal_face_count=11")
            && diag
                .message
                .contains("control_volume_boundary_face_count=8")
            && diag
                .message
                .contains("control_volume_connectivity_coverage_ratio=1")
            && diag.message.contains("domain_length_m=2.4")
            && diag.message.contains("face_area_m2=0.04")
            && diag.message.contains("face_area_m2=")
            && diag.message.contains("control_volume_volume_m3=")
            && diag.message.contains("courant_number=")
            && diag.message.contains("active_dimension_count=3")
            && diag.message.contains("element_geometry_node_count=4")
            && diag.message.contains("element_geometry_edge_count=5")
            && diag.message.contains("element_geometry_coverage_ratio=1")
            && diag
                .message
                .contains("element_topology_sample_element_count=2")
            && diag
                .message
                .contains("element_topology_sample_edge_count=5")
    }));
}

#[test]
fn analysis_run_cfd_rejects_partial_authored_boundary_conditions() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps[0].kind = AnalysisStepKind::Cfd;
    model.cfd = Some(sample_cfd_domain(CfdSolveFamily::SteadyState, true));
    model.boundary_conditions = vec![BoundaryCondition {
        bc_id: "bc_cfd_inlet_only".to_string(),
        region_id: "fluid_inlet".to_string(),
        kind: BoundaryConditionKind::CfdInletVelocity {
            velocity_m_per_s: 4.25,
        },
    }];

    let err = analysis_run_cfd_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect_err("partial authored cfd boundaries should fail validation");

    assert_eq!(err.operation, "fea.run_cfd");
    assert_eq!(err.op_version, "fea.run_cfd/v1");
    assert_eq!(err.error_code, "RM.FEA.RUN_CFD.INVALID_BOUNDARY_CONDITIONS");
}

#[test]
fn analysis_run_cht_returns_coupled_payload_and_diagnostics() {
    let _guard = analysis_test_guard();
    let model = sample_cht_model();

    let envelope = analysis_run_cht_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisChtRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 4,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("cht run should return envelope");

    assert_eq!(envelope.operation, "fea.run_cht");
    assert_eq!(envelope.op_version, "fea.run_cht/v1");
    assert_eq!(envelope.data.run.solver_method, "cht_conjugate_projection");
    assert!(envelope.data.transient_results.is_none());
    assert!(envelope.data.thermal_results.is_some());
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CFD_FLOW"
            && diag.message.contains("reynolds_number=")
            && diag.message.contains("topology_basis=implicit_channel")));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_ASSEMBLY"
            && diag.message.contains("topology_basis=implicit_channel")
            && diag.message.contains("control_volume_count=")
            && diag.message.contains("domain_length_m=")
    }));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CHT_COUPLING"
            && diag.message.contains("applied_temperature_delta_k=60")));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CHT_INTERFACE_CLOSURE"
            && diag.message.contains("interface_face_count=")
            && diag.message.contains("max_temperature_jump_k=")
            && diag.message.contains("max_energy_residual=")
            && diag.message.contains("heat_flux_balance_ratio=")
            && diag.message.contains("thermal_transport_residual_ratio=")
            && diag
                .message
                .contains("interface_temperature_continuity_ratio=")
            && diag.message.contains("max_advection_temperature_shift_k=")
            && diag.message.contains("interface_conductance_w_per_m2k=")
            && diag
                .message
                .contains("flux_temperature_law_residual_ratio=")
            && diag
                .message
                .contains("heat_flux_realization_residual_ratio=")
            && diag.message.contains("coupled_interface_iteration_count=")
            && diag.message.contains("coupled_interface_residual_ratio=")
            && diag.message.contains("thermal_network_node_count=")
            && diag.message.contains("thermal_network_edge_count=")
            && diag
                .message
                .contains("interface_connectivity_coverage_ratio=")
            && diag
                .message
                .contains("mesh_backed_interface_connectivity_ratio=")
            && diag.message.contains("full_topology_edge_count=")
            && diag.message.contains("full_topology_element_count=")
            && diag.message.contains("thermal_network_residual_ratio=")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CHT_KNOWN_ANSWER"
            && diag.message.contains("basis=heated_channel_conjugate_slab")
            && diag
                .message
                .contains("heated_channel_energy_residual_ratio=")
            && diag
                .message
                .contains("conjugate_slab_flux_law_residual_ratio=")
            && diag.message.contains("advection_shift_coverage_ratio=")
            && diag.message.contains("coupled_interface_residual_ratio=")
            && diag
                .message
                .contains("heat_flux_realization_residual_ratio=")
            && diag
                .message
                .contains("interface_connectivity_coverage_ratio=")
            && diag
                .message
                .contains("mesh_backed_interface_connectivity_ratio=")
            && diag.message.contains("thermal_network_residual_ratio=")
            && diag.message.contains("known_answer_coverage_ratio=")
    }));
    let thermal = envelope
        .data
        .thermal_results
        .as_ref()
        .expect("thermal payload should exist");
    assert_eq!(thermal.time_points_s.len(), 4);
    let fluid_temperature = envelope
        .data
        .run
        .field(&fea_cht_fluid_temperature_field_id(0))
        .expect("cht fluid temperature field should be present");
    let solid_temperature = envelope
        .data
        .run
        .field(&fea_cht_solid_temperature_field_id(0))
        .expect("cht solid temperature field should be present");
    let interface_heat_flux = envelope
        .data
        .run
        .field(&fea_cht_interface_heat_flux_field_id(0))
        .expect("cht interface heat-flux field should be present");
    let interface_temperature_jump = envelope
        .data
        .run
        .field(&fea_cht_interface_temperature_jump_field_id(0))
        .expect("cht interface temperature jump field should be present");
    let thermal_flux_face_count = thermal
        .heat_flux_snapshots
        .first()
        .and_then(|field| field.shape.first().copied())
        .expect("thermal heat-flux snapshot should carry a recovery domain");
    let expected_interface_face_count =
        fluid_interface_face_count(&CfdDomainTopology::from_model(&model, None));
    assert_eq!(solid_temperature.shape, fluid_temperature.shape);
    assert!(expected_interface_face_count >= thermal_flux_face_count);
    assert_eq!(
        interface_heat_flux.shape,
        vec![expected_interface_face_count]
    );
    assert_eq!(interface_temperature_jump.shape, interface_heat_flux.shape);
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("cht results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("CHT descriptor should be present")
    };
    let velocity_descriptor = descriptor(FEA_FIELD_CHT_FLUID_VELOCITY);
    assert_eq!(velocity_descriptor.kind, AnalysisFieldKind::Vector);
    assert_eq!(velocity_descriptor.component_count, Some(3));
    for field_id in [
        FEA_FIELD_CHT_FLUID_PRESSURE.to_string(),
        fea_cht_fluid_temperature_field_id(0),
        fea_cht_solid_temperature_field_id(0),
        fea_cht_interface_temperature_jump_field_id(0),
        fea_cht_energy_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    let interface_heat_flux_descriptor = descriptor(&fea_cht_interface_heat_flux_field_id(0));
    assert_eq!(
        interface_heat_flux_descriptor.kind,
        AnalysisFieldKind::Scalar
    );
    assert_eq!(interface_heat_flux_descriptor.component_count, None);
}

#[test]
fn analysis_run_fsi_returns_coupled_payload_and_diagnostics() {
    let _guard = analysis_test_guard();
    let model = sample_fsi_model();

    let envelope = analysis_run_fsi_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisFsiRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 1.0e-3,
            step_count: 4,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_warn_threshold: 1.0e-4,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("fsi run should return envelope");

    assert_eq!(envelope.operation, "fea.run_fsi");
    assert_eq!(envelope.op_version, "fea.run_fsi/v1");
    assert_eq!(
        envelope.data.run.solver_method,
        "fsi_partitioned_projection"
    );
    assert!(envelope.data.transient_results.is_none());
    assert!(envelope.data.thermal_results.is_none());
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CFD_FLOW"
            && diag.message.contains("reynolds_number=")
            && diag.message.contains("topology_basis=implicit_channel")));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CFD_RESIDUAL"
            && diag.message.contains("max_momentum_residual=")));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_CFD_ASSEMBLY"
            && diag.message.contains("topology_basis=implicit_channel")
            && diag.message.contains("control_volume_count=")
            && diag.message.contains("domain_length_m=")
    }));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_FSI_INTERFACE_RESIDUAL"
            && diag.message.contains("max_interface_residual=")));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| diag.code
        == "FEA_FSI_INTERFACE_CLOSURE"
        && diag.message.contains("interface_node_count=")
        && diag.message.contains("interface_face_count=")
        && diag.message.contains("force_balance_ratio=")
        && diag
            .message
            .contains("max_displacement_transfer_residual_m=")
        && diag.message.contains("max_coupling_iteration_count=")
        && diag.message.contains("pressure_feedback_residual_ratio=")
        && diag.message.contains("two_way_interface_residual_ratio=")
        && diag
            .message
            .contains("structural_traction_update_residual_ratio=")
        && diag
            .message
            .contains("pressure_displacement_law_residual_ratio=")
        && diag.message.contains("structural_solve_residual_ratio=")
        && diag.message.contains("interface_work_j_per_m2=")
        && diag.message.contains("structural_strain_energy_j_per_m2=")
        && diag
            .message
            .contains("interface_work_energy_residual_ratio=")
        && diag.message.contains("structural_coupling_edge_count=")
        && diag
            .message
            .contains("interface_connectivity_coverage_ratio=")
        && diag
            .message
            .contains("mesh_backed_interface_connectivity_ratio=")
        && diag.message.contains("full_topology_edge_count=")
        && diag.message.contains("full_topology_element_count=")
        && diag.message.contains("interface_stiffness_pa_per_m=")));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_FSI_KNOWN_ANSWER"
            && diag
                .message
                .contains("basis=pressure_loaded_wall_partitioned")
            && diag
                .message
                .contains("pressure_loaded_wall_displacement_law_residual_ratio=")
            && diag
                .message
                .contains("interface_traction_balance_residual_ratio=")
            && diag
                .message
                .contains("partitioned_pressure_feedback_residual_ratio=")
            && diag.message.contains("two_way_interface_residual_ratio=")
            && diag
                .message
                .contains("structural_traction_update_residual_ratio=")
            && diag.message.contains("structural_solve_residual_ratio=")
            && diag
                .message
                .contains("interface_work_energy_residual_ratio=")
            && diag
                .message
                .contains("interface_connectivity_coverage_ratio=")
            && diag
                .message
                .contains("mesh_backed_interface_connectivity_ratio=")
            && diag.message.contains("known_answer_coverage_ratio=")));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_FSI_COUPLING"
            && diag.message.contains("cfd_profile_point_count=2")
            && diag.message.contains("authored_interface_count=")
            && diag.message.contains("interface_node_count=")
            && diag.message.contains("interface_face_count=")));
    let interface_pressure = envelope
        .data
        .run
        .field(&fea_fsi_interface_pressure_field_id(0))
        .expect("fsi interface pressure field should be present");
    let interface_traction = envelope
        .data
        .run
        .field(&fea_fsi_interface_traction_field_id(0))
        .expect("fsi interface traction field should be present");
    let interface_displacement = envelope
        .data
        .run
        .field(&fea_fsi_interface_displacement_field_id(0))
        .expect("fsi interface displacement field should be present");
    assert_eq!(interface_pressure.shape.len(), 1);
    assert_eq!(
        interface_traction.shape,
        vec![interface_pressure.shape[0], 3]
    );
    assert_eq!(interface_displacement.shape.len(), 2);
    assert_eq!(interface_displacement.shape[1], 3);
    assert_eq!(
        interface_displacement.shape[0],
        interface_pressure.shape[0] + 1
    );
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("fsi results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("FSI descriptor should be present")
    };
    for field_id in [
        fea_fsi_fluid_velocity_field_id(0),
        fea_fsi_structural_displacement_field_id(0),
        fea_fsi_interface_traction_field_id(0),
        fea_fsi_interface_displacement_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Vector);
        assert_eq!(descriptor.component_count, Some(3));
    }
    for field_id in [
        fea_fsi_fluid_pressure_field_id(0),
        fea_fsi_interface_pressure_field_id(0),
        fea_fsi_interface_residual_field_id(0),
        fea_fsi_coupling_iteration_count_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
}

#[test]
fn cht_prepared_topology_uses_boundary_faces_for_interface_fields() {
    let _guard = analysis_test_guard();
    let model = sample_cht_model();
    let cfd_domain = model.cfd.as_ref().expect("cfd domain should exist");
    let prep_context = sample_analysis_run_prep_context();
    let topology = CfdDomainTopology::from_model(&model, Some(&prep_context));
    let thermo_context = to_fea_thermo_mechanical_context(model_thermo_coupling_options(&model));
    let thermal_run = run_thermal_with_options(
        &model,
        ComputeBackend::Cpu,
        ThermalSolveOptions {
            step_count: 4,
            time_step_s: 1.0e-3,
            residual_target: 1.0e-4,
            prep_context: to_fea_prep_context(Some(&prep_context), None),
            thermo_mechanical_context: thermo_context,
        },
    )
    .expect("thermal run should succeed");

    let (fields, closure) = build_cht_run_fields(
        cfd_domain,
        &topology,
        &thermal_run,
        cht_interface_conductance_w_per_m2k(&model),
        64,
        1.0e-8,
    );
    let heat_flux = fields
        .iter()
        .find(|field| field.field_id == fea_cht_interface_heat_flux_field_id(0))
        .expect("CHT heat flux field should be present");
    let temperature_jump = fields
        .iter()
        .find(|field| field.field_id == fea_cht_interface_temperature_jump_field_id(0))
        .expect("CHT temperature jump field should be present");

    assert_eq!(
        topology.basis,
        CfdDomainTopologyBasis::PrepControlVolumeConnectivity
    );
    assert_eq!(fluid_interface_face_count(&topology), 8);
    assert_eq!(closure.interface_face_count, 8);
    assert_eq!(closure.thermal_network_node_count, 8);
    assert_eq!(closure.thermal_network_edge_count, 11);
    assert_eq!(closure.interface_connectivity_coverage_ratio, 1.0);
    assert_eq!(closure.mesh_backed_interface_connectivity_ratio, 1.0);
    assert_eq!(closure.full_topology_edge_count, 5);
    assert_eq!(closure.full_topology_element_count, 2);
    assert_eq!(heat_flux.shape, vec![8]);
    assert_eq!(temperature_jump.shape, vec![8]);
}

#[test]
fn prepared_coupled_interface_graph_uses_element_topology_sample() {
    let model = sample_cht_model();
    let prep_context = sample_analysis_run_prep_context();
    let topology = CfdDomainTopology::from_model(&model, Some(&prep_context));
    let edges = coupled_interface_graph_edges_for_topology(
        &topology,
        fluid_interface_face_count(&topology),
    );

    assert!(edges.contains(&(0, 1)));
    assert!(edges.contains(&(1, 2)));
    assert!(edges.contains(&(0, 2)));
    assert!(edges.contains(&(2, 3)));
    assert!(edges.contains(&(3, 4)));
    assert!(edges.contains(&(2, 4)));
    assert_eq!(edges.len(), topology.control_volume_internal_face_count);
}

#[test]
fn fsi_prepared_topology_uses_boundary_faces_for_interface_fields() {
    let _guard = analysis_test_guard();
    let model = sample_fsi_model();
    let cfd_domain = model.cfd.as_ref().expect("cfd domain should exist");
    let prep_context = sample_analysis_run_prep_context();
    let topology = CfdDomainTopology::from_model(&model, Some(&prep_context));
    let (fluid_velocity, fluid_pressure) = recover_cfd_velocity_pressure(cfd_domain, &topology, 0);
    let (residual_momentum, residual_continuity) =
        cfd_residual_norms(&fluid_velocity, &fluid_pressure, cfd_domain, &topology, 4);
    let (fields, closure) = build_fsi_run_fields(
        cfd_domain,
        &topology,
        4,
        fsi_structural_compliance_per_pa(&model),
        64,
        1.0e-8,
        &residual_momentum,
        &residual_continuity,
    );
    let pressure = fields
        .iter()
        .find(|field| field.field_id == fea_fsi_interface_pressure_field_id(0))
        .expect("FSI interface pressure field should be present");
    let traction = fields
        .iter()
        .find(|field| field.field_id == fea_fsi_interface_traction_field_id(0))
        .expect("FSI interface traction field should be present");

    assert_eq!(
        topology.basis,
        CfdDomainTopologyBasis::PrepControlVolumeConnectivity
    );
    assert_eq!(fluid_interface_face_count(&topology), 8);
    assert_eq!(closure.interface_face_count, 8);
    assert_eq!(closure.structural_coupling_edge_count, 11);
    assert_eq!(closure.interface_connectivity_coverage_ratio, 1.0);
    assert_eq!(closure.mesh_backed_interface_connectivity_ratio, 1.0);
    assert_eq!(closure.full_topology_edge_count, 5);
    assert_eq!(closure.full_topology_element_count, 2);
    assert_eq!(pressure.shape, vec![8]);
    assert_eq!(traction.shape, vec![8, 3]);
}

#[test]
fn analysis_run_transient_with_options_controls_timeline() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let envelope = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            time_step_s: 2.0e-3,
            min_time_step_s: 2.0e-3,
            max_time_step_s: 2.0e-3,
            step_count: 3,
            max_linear_iters: 64,
            tolerance: 1.0e-8,
            residual_target: 1.0e-6,
            adaptive_time_step: false,
            max_step_retries: 0,
            adapt_min_scale: 0.8,
            adapt_max_scale: 1.25,
            adapt_growth_exponent: 0.35,
            adapt_retry_growth_cap: 1.05,
            adapt_nonconverged_shrink: 0.75,
            dt_bucket_rel_tolerance: 0.0,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("transient run should succeed with options");

    let transient = envelope
        .data
        .transient_results
        .as_ref()
        .expect("transient payload should exist");
    assert_eq!(transient.time_points_s.len(), 4);
    assert_eq!(transient.time_points_s[0], 0.0);
    assert!((transient.time_points_s[3] - 6.0e-3).abs() < 1.0e-12);
    assert!(envelope.data.provenance.deterministic_mode);
}

#[test]
fn analysis_run_transient_rejects_non_monotonic_thermo_time_profile() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 70.0,
            thermal_expansion_coefficient: 1.1e-5,
            field_artifact_id: None,
            field_source: Some(ThermoFieldSource {
                source_id: "field/transient-a".to_string(),
                revision: 1,
                interpolation_mode: Some(ThermoFieldInterpolationMode::Linear),
                expected_region_ids: vec!["tip".to_string()],
            }),
            region_temperature_deltas: vec![ThermoRegionTemperatureDelta {
                region_id: "tip".to_string(),
                temperature_delta_k: 70.0,
            }],
            time_profile: vec![
                ThermoTimeProfilePoint {
                    normalized_time: 0.8,
                    scale: 1.0,
                },
                ThermoTimeProfilePoint {
                    normalized_time: 0.5,
                    scale: 0.9,
                },
            ],
        },
    );

    let err = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect_err("non-monotonic thermo time profile should be rejected");

    assert_eq!(err.error_code, "RM.FEA.RUN_TRANSIENT.INVALID_OPTIONS");
}

#[test]
fn analysis_run_nonlinear_rejects_unknown_thermo_expected_region_ids() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 80.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: Some(ThermoFieldSource {
                source_id: "field/nonlinear-a".to_string(),
                revision: 2,
                interpolation_mode: Some(ThermoFieldInterpolationMode::Step),
                expected_region_ids: vec!["missing_region".to_string()],
            }),
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let err = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions::production_recommended(),
        OperationContext::new(None, None),
    )
    .expect_err("unknown thermo expected region should be rejected");

    assert_eq!(err.error_code, "RM.FEA.RUN_NONLINEAR.INVALID_OPTIONS");
}

#[test]
fn analysis_run_nonlinear_rejects_invalid_plasticity_constitutive_options() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_plasticity(
        &mut model,
        PlasticityConstitutiveOptions {
            enabled: true,
            yield_strain: -1.0,
            hardening_modulus_ratio: 0.1,
            saturation_exponent: 1.0,
        },
    );

    let err = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect_err("nonlinear run should reject invalid plasticity options");

    assert_eq!(err.error_code, "RM.FEA.RUN_NONLINEAR.INVALID_OPTIONS");
}

#[test]
fn analysis_run_nonlinear_rejects_invalid_contact_interface_options() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_contact(
        &mut model,
        ContactInterfaceOptions {
            enabled: true,
            penalty_stiffness_scale: 0.0,
            max_penetration_ratio: 0.01,
            friction_coefficient: 0.0,
        },
    );

    let err = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect_err("nonlinear run should reject invalid contact options");

    assert_eq!(err.error_code, "RM.FEA.RUN_NONLINEAR.INVALID_OPTIONS");
}

#[test]
fn analysis_run_transient_can_resolve_thermo_field_artifact() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let root = temp_artifact_root("transient-thermo-resolve").join("thermo-fields");
    let _root_guard = scoped_thermo_field_artifact_root(&root);
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).expect("create thermo field artifact root");
    let mut field_artifact = serde_json::json!({
        "schema_version": "fea_thermo_field_artifact/v1",
        "source_geometry_id": model.geometry_id,
        "source_geometry_revision": model.geometry_revision,
        "artifact_status": "approved",
        "approved_by": "release-bot",
        "field_source": {
            "source_id": "artifact/transient-field",
            "revision": 1,
            "interpolation_mode": "linear",
            "expected_region_ids": [],
        },
        "region_temperature_deltas": [
            {"region_id": "tip", "temperature_delta_k": 72.0}
        ],
        "time_profile": [
            {"normalized_time": 0.0, "scale": 0.5},
            {"normalized_time": 1.0, "scale": 1.0}
        ]
    });
    let artifact_hash = thermo_field_payload_hash(
        &serde_json::from_value(field_artifact.clone()).expect("decode artifact struct"),
    );
    field_artifact["payload_hash"] = serde_json::Value::String(artifact_hash.clone());
    field_artifact["signature"] = serde_json::Value::String(thermo_field_signature(
        &artifact_hash,
        "release-bot",
        "runmat-dev-thermo-signing-key",
    ));
    fs::write(
        root.join("field_ok.json"),
        serde_json::to_vec_pretty(&field_artifact).expect("encode thermo field artifact"),
    )
    .expect("write thermo field artifact");
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 70.0,
            thermal_expansion_coefficient: 1.1e-5,
            field_artifact_id: Some("field_ok".to_string()),
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );
    let run = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect("transient run should resolve thermo field artifact");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    let _ = fs::remove_dir_all(&root);

    assert!(results.data.summary.thermo_spatial_coverage_ratio.is_some());
    assert_eq!(results.data.summary.thermo_region_delta_count, Some(1.0));
}

#[test]
fn analysis_run_transient_rejects_missing_thermo_field_artifact() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let root = temp_artifact_root("transient-thermo-missing").join("thermo-fields");
    let _root_guard = scoped_thermo_field_artifact_root(&root);
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).expect("create empty thermo field artifact root");
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 70.0,
            thermal_expansion_coefficient: 1.1e-5,
            field_artifact_id: Some("missing".to_string()),
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let err = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect_err("missing thermo field artifact should be rejected");
    let _ = fs::remove_dir_all(&root);

    assert_eq!(err.error_code, "RM.FEA.RUN_THERMO_FIELD.NOT_FOUND");
}

#[test]
fn analysis_run_transient_artifact_backed_thermo_matches_inline_profile() {
    let _guard = analysis_test_guard();
    let mut model = sample_model_with_material_assignment_mismatch();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];

    let root = temp_artifact_root("transient-thermo-inline-parity").join("thermo-fields");
    let _root_guard = scoped_thermo_field_artifact_root(&root);
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).expect("create thermo field artifact root");
    let mut inline_equivalent_artifact = serde_json::json!({
        "schema_version": "fea_thermo_field_artifact/v1",
        "source_geometry_id": model.geometry_id,
        "source_geometry_revision": model.geometry_revision,
        "artifact_status": "approved",
        "approved_by": "release-bot",
        "field_source": {
            "source_id": "artifact/inline-equivalent",
            "revision": 1,
            "interpolation_mode": "linear",
            "expected_region_ids": []
        },
        "region_temperature_deltas": [
            {"region_id": "tip", "temperature_delta_k": 90.0}
        ],
        "time_profile": [
            {"normalized_time": 0.0, "scale": 0.4},
            {"normalized_time": 1.0, "scale": 1.0}
        ]
    });
    let inline_hash = thermo_field_payload_hash(
        &serde_json::from_value(inline_equivalent_artifact.clone())
            .expect("decode inline artifact struct"),
    );
    inline_equivalent_artifact["payload_hash"] = serde_json::Value::String(inline_hash.clone());
    inline_equivalent_artifact["signature"] = serde_json::Value::String(thermo_field_signature(
        &inline_hash,
        "release-bot",
        "runmat-dev-thermo-signing-key",
    ));
    fs::write(
        root.join("inline_equivalent.json"),
        serde_json::to_vec_pretty(&inline_equivalent_artifact).expect("encode artifact"),
    )
    .expect("write artifact");

    let mut inline_model = model.clone();
    set_model_thermo_coupling(
        &mut inline_model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: vec![ThermoRegionTemperatureDelta {
                region_id: "tip".to_string(),
                temperature_delta_k: 90.0,
            }],
            time_profile: vec![
                ThermoTimeProfilePoint {
                    normalized_time: 0.0,
                    scale: 0.4,
                },
                ThermoTimeProfilePoint {
                    normalized_time: 1.0,
                    scale: 1.0,
                },
            ],
        },
    );
    let inline = analysis_run_transient_with_options_op(
        &inline_model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect("inline thermo run should succeed");

    let mut artifact_model = model;
    set_model_thermo_coupling(
        &mut artifact_model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: Some("inline_equivalent".to_string()),
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );
    let artifact_backed = analysis_run_transient_with_options_op(
        &artifact_model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions::default(),
        OperationContext::new(None, None),
    )
    .expect("artifact-backed thermo run should succeed");
    let _ = fs::remove_dir_all(&root);

    let inline_transient = inline
        .data
        .transient_results
        .as_ref()
        .expect("inline transient payload");
    let artifact_transient = artifact_backed
        .data
        .transient_results
        .as_ref()
        .expect("artifact transient payload");
    let inline_final = inline_transient
        .displacement_snapshots
        .last()
        .and_then(|field| field.as_host_f64())
        .expect("inline host displacement");
    let artifact_final = artifact_transient
        .displacement_snapshots
        .last()
        .and_then(|field| field.as_host_f64())
        .expect("artifact host displacement");
    assert_eq!(inline_final.len(), artifact_final.len());
    let mut max_abs = 0.0_f64;
    for (lhs, rhs) in inline_final.iter().zip(artifact_final.iter()) {
        max_abs = max_abs.max((lhs - rhs).abs());
    }
    assert!(max_abs <= 1.0e-12);
}

#[test]
fn transient_balanced_degrades_when_thermo_mechanical_severity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    for material in &mut model.materials {
        material.thermal.expansion_coefficient_per_k = 1.0e-3;
    }
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.0e-3,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions {
            quality_policy: QualityPolicy::Balanced,
            adaptive_time_step: true,
            step_count: 8,
            ..AnalysisTransientRunOptions::default()
        },
        OperationContext::new(None, None),
    )
    .expect("transient run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::ThermoMechanicalTransientStress));

    let transient = run
        .data
        .transient_results
        .as_ref()
        .expect("transient results should be present");
    assert_eq!(
        transient.thermo_mechanical_temperature_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.thermo_mechanical_thermal_strain_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.thermo_mechanical_thermal_stress_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.thermo_mechanical_displacement_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.thermo_mechanical_von_mises_snapshots.len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient
            .thermo_mechanical_coupling_residual_snapshots
            .len(),
        transient.time_points_s.len()
    );
    assert_eq!(
        transient.thermo_mechanical_temperature_snapshots[0].field_id,
        fea_thermo_mechanical_temperature_field_id(0)
    );
    assert_eq!(
        transient.thermo_mechanical_thermal_strain_snapshots[0].field_id,
        fea_thermo_mechanical_thermal_strain_field_id(0)
    );
    assert_eq!(
        transient.thermo_mechanical_thermal_stress_snapshots[0].field_id,
        fea_thermo_mechanical_thermal_stress_field_id(0)
    );
    assert_eq!(
        transient.thermo_mechanical_displacement_snapshots[0].field_id,
        fea_thermo_mechanical_displacement_field_id(0)
    );
    assert_eq!(
        transient.thermo_mechanical_von_mises_snapshots[0].field_id,
        fea_thermo_mechanical_von_mises_field_id(0)
    );
    assert_eq!(
        transient.thermo_mechanical_coupling_residual_snapshots[0].field_id,
        fea_thermo_mechanical_coupling_residual_field_id(0)
    );

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("thermo-mechanical transient results should be queryable");
    let descriptor = |field_id: &str| {
        results
            .data
            .field_descriptors
            .iter()
            .find(|descriptor| descriptor.field_id == field_id)
            .expect("transient thermo-mechanical descriptor should be present")
    };
    for field_id in [
        fea_thermo_mechanical_temperature_field_id(0),
        fea_thermo_mechanical_von_mises_field_id(0),
        fea_thermo_mechanical_coupling_residual_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Scalar);
        assert_eq!(descriptor.component_count, None);
    }
    let displacement_descriptor = descriptor(&fea_thermo_mechanical_displacement_field_id(0));
    assert_eq!(displacement_descriptor.kind, AnalysisFieldKind::Vector);
    assert_eq!(displacement_descriptor.component_count, Some(3));
    for field_id in [
        fea_thermo_mechanical_thermal_strain_field_id(0),
        fea_thermo_mechanical_thermal_stress_field_id(0),
    ] {
        let descriptor = descriptor(&field_id);
        assert_eq!(descriptor.kind, AnalysisFieldKind::Tensor);
        assert_eq!(descriptor.component_count, Some(6));
    }
}

#[test]
fn transient_balanced_degrades_when_thermo_heterogeneity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model_with_material_assignment_mismatch();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    set_model_thermo_coupling(
        &mut model,
        ThermoMechanicalCouplingOptions {
            enabled: true,
            reference_temperature_k: 293.15,
            applied_temperature_delta_k: 90.0,
            thermal_expansion_coefficient: 1.2e-5,
            field_artifact_id: None,
            field_source: None,
            region_temperature_deltas: Vec::new(),
            time_profile: Vec::new(),
        },
    );

    let run = analysis_run_transient_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisTransientRunOptions {
            quality_policy: QualityPolicy::Balanced,
            adaptive_time_step: true,
            step_count: 8,
            ..AnalysisTransientRunOptions::default()
        },
        OperationContext::new(None, None),
    )
    .expect("transient run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run.data.quality_reasons.iter().any(|reason| {
        reason.code == QualityReasonCode::ThermoMechanicalConstitutiveSpreadHigh
            || reason.code == QualityReasonCode::ThermoMechanicalAssignmentHeterogeneityHigh
    }));
}

#[test]
fn nonlinear_balanced_degrades_when_plasticity_severity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_plasticity(
        &mut model,
        PlasticityConstitutiveOptions {
            enabled: true,
            yield_strain: 2.0e-4,
            hardening_modulus_ratio: 0.2,
            saturation_exponent: 4.0,
        },
    );

    let run = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            quality_policy: QualityPolicy::Balanced,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::PlasticityNonlinearStress));
    assert!(run
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_PLASTIC_NONLINEAR"));
}

#[test]
fn nonlinear_balanced_degrades_when_contact_severity_is_high() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "nonlinear_1".to_string(),
        kind: AnalysisStepKind::Nonlinear,
    }];
    set_model_contact(
        &mut model,
        ContactInterfaceOptions {
            enabled: true,
            penalty_stiffness_scale: 0.15,
            max_penetration_ratio: 0.035,
            friction_coefficient: 0.9,
        },
    );

    let run = analysis_run_nonlinear_with_options_op(
        &model,
        ComputeBackend::Cpu,
        AnalysisNonlinearRunOptions {
            quality_policy: QualityPolicy::Balanced,
            ..AnalysisNonlinearRunOptions::balanced()
        },
        OperationContext::new(None, None),
    )
    .expect("nonlinear run should return envelope");

    assert!(!run.data.publishable);
    assert_eq!(run.data.run_status, RunStatus::Degraded);
    assert!(run
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::ContactNonlinearStress));
    assert!(run
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_CONTACT_NONLINEAR"));
}

#[test]
fn analysis_run_modal_returns_native_modal_result() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let modal_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model_run".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal model should be created");

    let envelope = analysis_run_modal_op(
        &modal_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("modal run should produce modal result");

    assert_eq!(envelope.operation, "fea.run_modal");
    assert_eq!(envelope.op_version, "fea.run_modal/v1");
    assert_eq!(
        envelope.data.run.solver_method,
        "matrix_free_subspace_iteration"
    );
    assert_eq!(
        envelope.data.provenance.solver_method,
        "matrix_free_subspace_iteration"
    );
    assert_eq!(envelope.data.run_status, RunStatus::Degraded);
    assert!(!envelope.data.publishable);
    let modal = envelope
        .data
        .modal_results
        .as_ref()
        .expect("modal payload should exist");
    assert!(!modal.eigenvalues_hz.is_empty());
    assert_eq!(modal.eigenvalues_hz.len(), modal.mode_shapes.len());
    assert_eq!(
        modal.mode_shapes[0].field_id,
        fea_modal_mode_shape_field_id(1)
    );
    assert_eq!(modal.mode_shapes[0].shape.len(), 2);
    assert_eq!(modal.mode_shapes[0].shape[1], 3);
    assert_eq!(modal.eigenvalues_hz.len(), modal.residual_norms.len());
    assert!(modal.residual_norms.iter().all(|value| value.is_finite()));
    assert_eq!(modal.modal_payload_version, "modal_results/v1");
    assert_eq!(modal.mode_units, ModalFrequencyUnits::Hz);
    assert_eq!(modal.frequency_basis, ModalFrequencyBasis::NativeEigenSolve);
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("modal results should be queryable");
    let field_ids = results
        .data
        .field_descriptors
        .iter()
        .map(|descriptor| descriptor.field_id.as_str())
        .collect::<Vec<_>>();
    assert!(field_ids.contains(&FEA_FIELD_MODAL_FREQUENCY_HZ));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_EIGENVALUE));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_MODAL_MASS));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_MODAL_STIFFNESS));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_PARTICIPATION_FACTOR));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_RESIDUAL_NORM));
    assert!(field_ids.contains(&FEA_FIELD_MODAL_RELATIVE_FREQUENCY_SEPARATION));
    let orthogonality_descriptor = results
        .data
        .field_descriptors
        .iter()
        .find(|descriptor| descriptor.field_id == FEA_FIELD_MODAL_M_ORTHOGONALITY)
        .expect("modal orthogonality descriptor should be present");
    assert_eq!(orthogonality_descriptor.kind, AnalysisFieldKind::Tensor);
    assert_eq!(
        orthogonality_descriptor.shape,
        vec![modal.eigenvalues_hz.len(), modal.eigenvalues_hz.len()]
    );
    assert_eq!(orthogonality_descriptor.component_count, None);
    assert!(envelope
        .data
        .quality_reasons
        .iter()
        .any(|reason| reason.code == QualityReasonCode::ModalResidualExceeded));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_MODAL_CONVERGENCE"));
}

#[test]
fn analysis_run_acoustic_returns_acoustic_fields_and_diagnostics() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let acoustic_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "acoustic_model_run".to_string(),
            profile: AnalysisCreateModelProfile::AcousticHarmonic,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("acoustic model should be created");

    let envelope = analysis_run_acoustic_op(
        &acoustic_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("acoustic run should produce acoustic fields");

    assert_eq!(envelope.operation, "fea.run_acoustic");
    assert_eq!(envelope.op_version, "fea.run_acoustic/v1");
    assert_eq!(
        envelope.data.run.solver_method,
        "acoustic_domain_graph_helmholtz_harmonic"
    );
    assert!(envelope.data.modal_results.is_none());
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_ACOUSTIC_HARMONIC_RESPONSE"));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_ACOUSTIC_DOMAIN_ASSEMBLY"));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_ACOUSTIC_DOMAIN_ASSEMBLY"
            && diag.message.contains("domain_edge_count=2")
            && diag.message.contains("domain_active_dimension_count=2")
    }));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_ACOUSTIC_HELMHOLTZ_RESIDUAL"));
    assert!(envelope
        .data
        .run
        .diagnostics
        .iter()
        .any(|diag| diag.code == "FEA_ACOUSTIC_FREQUENCY_RESPONSE"));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_ACOUSTIC_KNOWN_ANSWER"
            && diag.message.contains("tube_mode_alignment_error_ratio=")
            && diag.message.contains("tube_pressure_variation_ratio=")
            && diag.message.contains("cavity_mode_spacing_ratio=")
            && diag.message.contains("known_answer_coverage_ratio=1")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_ACOUSTIC_HARMONIC_RESPONSE"
            && diag.message.contains("acoustic_material_coverage_ratio=1")
    }));
    assert!(envelope.data.run.diagnostics.iter().any(|diag| {
        diag.code == "FEA_ACOUSTIC_BOUNDARY_MODEL"
            && diag.message.contains("acoustic_boundary_coverage_ratio=1")
            && diag.message.contains("rigid_wall_count=1")
    }));
    assert!(envelope
        .data
        .run
        .field(FEA_FIELD_ACOUSTIC_PRESSURE_REAL)
        .is_some());
    assert!(envelope
        .data
        .run
        .field(FEA_FIELD_ACOUSTIC_PRESSURE_MAGNITUDE)
        .is_some());
    assert!(envelope
        .data
        .run
        .field(FEA_FIELD_ACOUSTIC_PARTICLE_VELOCITY)
        .is_some());
    let response_field_count = envelope
        .data
        .run
        .fields
        .iter()
        .filter(|field| field.field_id.starts_with("acoustic.frequency_response."))
        .count();
    assert_eq!(response_field_count, 3);
    let expected_first_response_hz = 125.0_f64 * 3.0 * 3.0_f64.sqrt() * 0.75;
    assert!(envelope
        .data
        .run
        .field(&fea_acoustic_frequency_response_field_id(
            expected_first_response_hz
        ))
        .is_some());
    let results = analysis_results_op(
        &envelope.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("acoustic results should be queryable");
    let field_ids = results
        .data
        .field_descriptors
        .iter()
        .map(|descriptor| descriptor.field_id.as_str())
        .collect::<Vec<_>>();
    assert!(field_ids.contains(&FEA_FIELD_ACOUSTIC_PRESSURE_REAL));
    assert!(field_ids.contains(&FEA_FIELD_ACOUSTIC_PRESSURE_MAGNITUDE));
    assert!(field_ids.contains(&FEA_FIELD_ACOUSTIC_PARTICLE_VELOCITY));
    assert!(field_ids
        .iter()
        .any(|field_id| field_id.starts_with("acoustic.frequency_response.")));
}

#[test]
fn analysis_run_modal_with_options_controls_requested_mode_count() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let modal_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model_run_opts".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal model should be created");

    let envelope = analysis_run_modal_with_options_op(
        &modal_model.data,
        ComputeBackend::Cpu,
        AnalysisModalRunOptions {
            deterministic_mode: true,
            precision_mode: PrecisionMode::Fp64,
            quality_policy: QualityPolicy::Balanced,
            mode_count: 2,
            residual_warn_threshold: 1.0e-2,
            prep_context: None,
            prep_artifact_id: None,
            prep_calibration_profile: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal run should succeed with options");

    let modal = envelope
        .data
        .modal_results
        .as_ref()
        .expect("modal payload should exist");
    assert!(!modal.eigenvalues_hz.is_empty());
    assert!(modal.eigenvalues_hz.len() <= 2);
    assert!(envelope.data.provenance.deterministic_mode);
}

#[test]
fn analysis_results_include_modal_payload_for_modal_runs() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let modal_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model_results".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal model should be created");

    let run = analysis_run_modal_op(
        &modal_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("modal run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    let modal = results
        .data
        .modal_results
        .as_ref()
        .expect("modal payload should propagate to results");
    assert!(!modal.eigenvalues_hz.is_empty());
    assert_eq!(modal.eigenvalues_hz.len(), modal.mode_shapes.len());
    assert_eq!(modal.mode_shapes[0].shape.len(), 2);
    assert_eq!(modal.mode_shapes[0].shape[1], 3);
    assert_eq!(modal.eigenvalues_hz.len(), modal.residual_norms.len());
    assert_eq!(modal.modal_payload_version, "modal_results/v1");
    assert_eq!(modal.mode_units, ModalFrequencyUnits::Hz);
    assert_eq!(modal.frequency_basis, ModalFrequencyBasis::NativeEigenSolve);
    assert!(results.data.summary.mode_count > 0);
    assert_eq!(
        results.data.summary.mode_count,
        results.data.summary.available_mode_indices.len()
    );
    assert!(results.data.summary.min_frequency_hz.is_some());
    assert!(results.data.summary.max_frequency_hz.is_some());
    assert!(results.data.summary.max_modal_residual_norm.is_some());
    assert!(results.data.summary.first_mode_converged.is_some());
}

#[test]
fn analysis_results_query_can_exclude_modal_payload() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let modal_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model_results_filter".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal model should be created");
    let run = analysis_run_modal_op(
        &modal_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("modal run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: Vec::new(),
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: false,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    assert!(results.data.modal_results.is_none());
}

#[test]
fn analysis_results_query_rejects_unknown_modal_mode_index() {
    let _guard = analysis_test_guard();
    let geometry = sample_geometry_asset();
    let modal_model = analysis_create_model_op(
        &geometry,
        AnalysisCreateModelIntentSpec {
            model_id: "modal_model_results_index".to_string(),
            profile: AnalysisCreateModelProfile::ModalStructural,
            prep_context: None,
        },
        OperationContext::new(None, None),
    )
    .expect("modal model should be created");
    let run = analysis_run_modal_op(
        &modal_model.data,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("modal run should succeed");

    let err = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: Vec::new(),
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: vec![10],
            include_transient_results: true,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(None, None),
    )
    .expect_err("results should fail for unknown mode index");

    assert_eq!(err.error_code, "RM.FEA.RESULTS.MODE_NOT_FOUND");
    assert_eq!(err.operation, "fea.results");
    assert_eq!(err.op_version, "fea.results/v1");
}

#[test]
fn analysis_results_include_transient_payload_for_transient_runs() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    let run = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("transient run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    let transient = results
        .data
        .transient_results
        .as_ref()
        .expect("transient payload should propagate");
    assert_eq!(
        transient.integration_method,
        TransientIntegrationMethod::ImplicitEuler
    );
    assert!(!transient.time_points_s.is_empty());
    assert_eq!(
        transient.time_points_s.len(),
        transient.displacement_snapshots.len()
    );
}

#[test]
fn analysis_results_include_electromagnetic_fields_for_em_runs() {
    let _guard = analysis_test_guard();
    let spec = sample_electromagnetic_study_spec();
    let run = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("electromagnetic study should run");

    let results = analysis_results_by_run_id_op(
        &run.data.run_id,
        AnalysisResultsQuery::default(),
        OperationContext::new(None, None),
    )
    .expect("electromagnetic results should load");
    let field_ids = results
        .data
        .field_descriptors
        .iter()
        .map(|field| field.field_id.as_str())
        .collect::<Vec<_>>();

    assert!(field_ids.contains(&FEA_FIELD_EM_VECTOR_POTENTIAL_REAL));
    assert!(field_ids.contains(&FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_MAGNITUDE));
}

#[cfg(feature = "plot-core")]
fn solver_topology_counts(run_id: &str) -> (usize, usize) {
    let persisted = storage::load_run_result(run_id)
        .expect("run load should succeed")
        .expect("run should be persisted");
    let topology = persisted
        .render_topology
        .as_ref()
        .expect("run should persist render topology");
    (
        topology
            .meshes
            .iter()
            .map(|mesh| mesh.vertices.len())
            .sum::<usize>(),
        topology
            .meshes
            .iter()
            .map(|mesh| mesh.triangles.len())
            .sum::<usize>(),
    )
}

#[cfg(feature = "plot-core")]
fn assert_any_solver_topology_overlay(figures: &[AnalysisGeneratedFigure], run_id: &str) {
    let (vertex_count, triangle_count) = solver_topology_counts(run_id);
    let overlay_counts = figures
        .iter()
        .filter(|figure| matches!(figure.kind, AnalysisGeneratedFigureKind::MeshResult))
        .flat_map(|figure| figure.figure.plots())
        .filter_map(|plot| match plot {
            runmat_plot::plots::PlotElement::Mesh(mesh) => Some(mesh),
            _ => None,
        })
        .map(|mesh| {
            mesh.scalar_field()
                .map(|field| field.values.len())
                .or_else(|| mesh.vector_field().map(|field| field.vectors.len()))
                .unwrap_or(0)
        })
        .filter(|count| *count > 0)
        .collect::<Vec<_>>();
    assert!(
        overlay_counts
            .iter()
            .any(|count| *count == vertex_count || *count == triangle_count),
        "expected at least one overlay count to match solver topology vertices={} or triangles={}, got {:?}",
        vertex_count,
        triangle_count,
        overlay_counts
    );
}

#[cfg(feature = "plot-core")]
#[test]
fn analysis_generate_study_run_figures_returns_mesh_figures() {
    let _guard = analysis_test_guard();
    let spec = sample_linear_static_study_spec();
    let run = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("linear static study should run");

    let figures = analysis_generate_study_run_figures(
        &spec,
        &run.data.run_id,
        AnalysisFigureGenerationOptions {
            include_comparison: false,
            include_trends: false,
            ..AnalysisFigureGenerationOptions::default()
        },
    )
    .expect("study figures should be generated");

    assert!(figures
        .iter()
        .any(|figure| matches!(figure.kind, AnalysisGeneratedFigureKind::MeshResult)));
    assert!(figures.iter().any(|figure| !figure.figure.is_empty()));
    let stress_figure = figures
        .iter()
        .find(|figure| {
            figure
                .field_ids
                .iter()
                .any(|field_id| field_id == FEA_FIELD_STRUCTURAL_VON_MISES)
        })
        .expect("von Mises figure should be generated");
    let persisted = storage::load_run_result(&run.data.run_id)
        .expect("run load should succeed")
        .expect("run should be persisted");
    let topology = persisted
        .render_topology
        .as_ref()
        .expect("run should persist render topology");
    let solver_triangle_count = topology
        .meshes
        .iter()
        .map(|mesh| mesh.triangles.len())
        .sum::<usize>();
    let overlay_triangle_count = stress_figure
        .figure
        .plots()
        .filter_map(|plot| match plot {
            runmat_plot::plots::PlotElement::Mesh(mesh) => mesh.scalar_field(),
            _ => None,
        })
        .filter(|field| {
            field.field_id == FEA_FIELD_STRUCTURAL_VON_MISES
                && field.location == runmat_plot::plots::MeshFieldLocation::Triangle
        })
        .map(|field| field.values.len())
        .sum::<usize>();
    assert_eq!(overlay_triangle_count, solver_triangle_count);
}

#[cfg(feature = "plot-core")]
#[test]
fn analysis_generate_study_run_figures_uses_solver_topology_for_em_fields() {
    let _guard = analysis_test_guard();
    let spec = sample_electromagnetic_study_spec();
    let run = analysis_run_study_op(&spec, OperationContext::new(None, None))
        .expect("electromagnetic study should run");

    let figures = analysis_generate_study_run_figures(
        &spec,
        &run.data.run_id,
        AnalysisFigureGenerationOptions {
            include_comparison: false,
            include_trends: false,
            ..AnalysisFigureGenerationOptions::default()
        },
    )
    .expect("study figures should be generated");

    assert_any_solver_topology_overlay(&figures, &run.data.run_id);
}

#[test]
fn analysis_results_query_can_exclude_transient_payload() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    let run = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("transient run should succeed");

    let results = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: Vec::new(),
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: false,
            transient_snapshot_indices: Vec::new(),
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(None, None),
    )
    .expect("results should succeed");

    assert!(results.data.transient_results.is_none());
    assert!(results.data.summary.snapshot_count > 0);
    assert_eq!(results.data.summary.time_start_s, Some(0.0));
    assert!(results.data.summary.time_end_s.unwrap_or(0.0) > 0.0);
    assert!(results.data.summary.max_transient_residual_norm.is_some());
    assert!(results.data.summary.final_step_converged.is_some());
}

#[test]
fn analysis_results_query_rejects_unknown_transient_snapshot_index() {
    let _guard = analysis_test_guard();
    let mut model = sample_model();
    model.steps = vec![AnalysisStep {
        step_id: "transient_1".to_string(),
        kind: AnalysisStepKind::Transient,
    }];
    let run = analysis_run_transient_op(
        &model,
        ComputeBackend::Cpu,
        OperationContext::new(None, None),
    )
    .expect("transient run should succeed");

    let err = analysis_results_op(
        &run.data,
        AnalysisResultsQuery {
            include_fields: Vec::new(),
            include_field_values: true,

            include_diagnostics: true,
            diagnostic_codes: Vec::new(),
            include_modal_results: true,
            mode_indices: Vec::new(),
            include_transient_results: true,
            transient_snapshot_indices: vec![999],
            include_nonlinear_results: true,
            include_electromagnetic_results: true,
        },
        OperationContext::new(None, None),
    )
    .expect_err("results should fail for unknown transient snapshot index");

    assert_eq!(
        err.error_code,
        "RM.FEA.RESULTS.TRANSIENT_SNAPSHOT_NOT_FOUND"
    );
    assert_eq!(err.operation, "fea.results");
    assert_eq!(err.op_version, "fea.results/v1");
}
