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

import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertTrue

/**
 * Unit tests for the velocity-dependent (multi-zone) ballistic coefficient feature
 * (P2 of BALLISTICS_ENGINE_ACCURACY_ANALYSIS.md).
 *
 * Reference numbers were cross-checked against an independent standard-G7 point-mass
 * solver (ballistics-test-data/tools/g7_reference.py).
 */
class MultiZoneBcTest {

    private fun baseParams(zones: List<BcZone> = emptyList()) = BallisticsParams(
        muzzleVelocityFps = 2700.0,
        bc                = 0.30,
        bcModel           = BcModel.G7,
        bcZones           = zones,
        zeroDistanceYards = 100.0,
        sightHeightIn     = 1.5,
    )

    private fun elev(params: BallisticsParams, dist: Double): Double =
        BallisticsEngine.calculateTrajectory(params, listOf(dist))[dist]!!.elevationMil

    @Test
    fun emptyZonesIdenticalToSingleBc() {
        // Empty bcZones must be bit-for-bit identical to the legacy single-BC path.
        val single = elev(baseParams(), 1000.0)
        val empty  = elev(baseParams(emptyList()), 1000.0)
        assertTrue(single == empty, "empty zones diverged from single BC: $single vs $empty")
    }

    @Test
    fun twoEqualZonesEqualSingleBc() {
        // Two bands with the same BC must reproduce the single-BC result exactly.
        val single = elev(baseParams(), 1000.0)
        val zoned  = elev(baseParams(listOf(BcZone(0.0, 0.30), BcZone(3000.0, 0.30))), 1000.0)
        assertTrue(abs(single - zoned) < 1e-9, "two equal zones diverged: $single vs $zoned")
    }

    @Test
    fun singleZoneFallsBackToSingleBc() {
        // A list with fewer than two valid bands is treated as "use the single bc field".
        val single = elev(baseParams(), 1000.0)
        val oneZone = elev(baseParams(listOf(BcZone(0.0, 0.30))), 1000.0)
        assertTrue(abs(single - oneZone) < 1e-9, "single zone diverged: $single vs $oneZone")
    }

    @Test
    fun degradingBcAddsLongRangeDrop() {
        // 0.30 above Mach 1.5 (~1675 fps at sea-level 59 °F), 0.24 below.
        // With BC=0.30 and MV=2700 fps the bullet crosses Mach 1.5 somewhere past
        // 1000 yd, so 1500 yd is used as the long-range checkpoint.
        // At 200 yd the bullet is well above Mach 1.5 — near-range elevation is unchanged.
        val zones      = listOf(BcZone(1675.0, 0.30), BcZone(0.0, 0.24))
        val d200Single = elev(baseParams(), 200.0)
        val d200Zoned  = elev(baseParams(zones), 200.0)
        val d15Single  = elev(baseParams(), 1500.0)
        val d15Zoned   = elev(baseParams(zones), 1500.0)

        assertTrue(abs(d200Zoned - d200Single) < 0.02,
            "200 yd should be ~unchanged (well above Mach 1.5): $d200Single vs $d200Zoned")
        val delta = d15Zoned - d15Single
        assertTrue(delta > 0.0,
            "degrading BC below Mach 1.5 must add drop by 1500 yd; got Δ=$delta mil")
    }

    @Test
    fun bandSelectionRespectsVelocityOrdering() {
        // Lower BC across the WHOLE flight (single band that always applies) must produce
        // strictly more drop than the higher BC at every distance.
        val low  = elev(baseParams(listOf(BcZone(0.0, 0.24), BcZone(100000.0, 0.24))), 1000.0)
        val high = elev(baseParams(listOf(BcZone(0.0, 0.30), BcZone(100000.0, 0.30))), 1000.0)
        assertTrue(low > high, "lower BC must yield more drop: low=$low high=$high")
    }
}
