// Copyright (C) 2026 Jason M. Schwefel. All Rights Reserved.
package com.coldboreballisticsllc.scopedope.ballistics

import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Regression tests pinning the engine's atmosphere model ([BallisticsEngine.airDensityRatio]
 * and [BallisticsEngine.speedOfSoundFps]) to known standard-atmosphere physics.
 *
 * Motivation: the cross-validation suite barely exercises the atmosphere model —
 * almost every case uses station pressure 0 (altitude path) and standard sea level,
 * so a density-path regression would slip through. These checks close that gap
 * directly. In particular they assert the **temperature term** that converts pressure
 * ratio into a true density ratio (σ = δ · T₀/T): a regression that returned a
 * pressure ratio instead of a density ratio would make the temperature cases below
 * collapse to 1.0 and fail here.
 *
 * Tolerances are deliberately loose enough to be agnostic to the exact standard-temp
 * constant (59 vs 60 °F) but tight enough to catch a pressure-vs-density swap.
 */
class AtmosphereModelTest {

    private val E = BallisticsEngine

    @Test
    fun seaLevelStandardIsUnity() {
        // Standard sea level: density ratio must be 1.0 by definition.
        val viaPressure = E.airDensityRatio(0.0, 59.0, 1013.25, 0.0)
        val viaAltitude = E.airDensityRatio(0.0, 59.0, 0.0, 0.0)   // altitude path, alt 0
        assertNear("sea-level via measured pressure", viaPressure, 1.0, 0.005)
        assertNear("sea-level via altitude path", viaAltitude, 1.0, 0.005)
    }

    @Test
    fun temperatureChangesDensity_provesItIsDensityNotPressureRatio() {
        val std = E.airDensityRatio(0.0, 59.0, 1013.25, 0.0)
        val hot = E.airDensityRatio(0.0, 100.0, 1013.25, 0.0)   // warmer → less dense
        val cold = E.airDensityRatio(0.0, 20.0, 1013.25, 0.0)   // colder → denser

        // If the model ignored temperature (a pressure-ratio bug), all three would be ~1.0.
        assertTrue("hot air must be less dense than standard ($hot !< $std)", hot < std - 0.02)
        assertTrue("cold air must be denser than standard ($cold !> $std)", cold > std + 0.02)
        // Physical magnitude at constant (standard) pressure: σ ≈ T₀/T.
        assertNear("100 °F density ratio", hot, 0.928, 0.01)
        assertNear("20 °F density ratio", cold, 1.083, 0.012)
    }

    @Test
    fun altitudeReducesDensityMonotonically() {
        val sl    = E.airDensityRatio(0.0, 59.0, 0.0, 0.0)
        val mid   = E.airDensityRatio(5000.0, 59.0, 0.0, 0.0)
        val high  = E.airDensityRatio(10000.0, 59.0, 0.0, 0.0)
        assertTrue("density must decrease with altitude (sl=$sl, 5k=$mid, 10k=$high)",
                   sl > mid && mid > high)
        // ICAO pressure at 10k ft ≈ 0.688 of sea level; with temp held at 59 °F, σ ≈ 0.69.
        assertNear("10000 ft density ratio @ 59 °F", high, 0.689, 0.02)
    }

    @Test
    fun measuredPressureBranchScalesWithPressure() {
        // Exercises the pressureHpa > 0 branch (untouched by every cross-validation case).
        val low  = E.airDensityRatio(0.0, 59.0, 850.0, 0.0)
        assertNear("850 hPa @ 59 °F density ratio", low, 0.840, 0.02)
        assertTrue("lower pressure → lower density", low < E.airDensityRatio(0.0, 59.0, 1013.25, 0.0))
    }

    @Test
    fun humidityReducesDensitySlightly() {
        // Moist air is lighter than dry air; effect is small (~1% at warm/humid).
        val dry = E.airDensityRatio(0.0, 86.0, 1013.25, 0.0)
        val wet = E.airDensityRatio(0.0, 86.0, 1013.25, 100.0)
        assertTrue("humid air must be less dense than dry ($wet !< $dry)", wet < dry)
        assertTrue("humidity effect should be small (<3%)", (dry - wet) < 0.03 * dry)
    }

    @Test
    fun speedOfSoundMatchesIsa() {
        assertNear("speed of sound at 59 °F", E.speedOfSoundFps(59.0), 1116.45, 0.5)
        assertTrue("speed of sound rises with temperature",
                   E.speedOfSoundFps(100.0) > E.speedOfSoundFps(32.0))
    }

    private fun assertNear(what: String, actual: Double, expected: Double, tol: Double) {
        assertTrue("$what: expected ≈ $expected (±$tol), got $actual",
                   kotlin.math.abs(actual - expected) <= tol)
    }
}
