defmodule Microwaveprop.Propagation.EmeTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Propagation.Eme

  describe "path_loss_db/2" do
    test "matches published amateur EME path-loss values at mean lunar distance" do
      # Round-trip one-way-to-moon + moon-to-earth path loss at
      # Earth–Moon mean distance. Cite spot values from the ARRL
      # Handbook EME chapter and W5LUA's moonbounce handbook.
      d = 384_400.0

      assert_in_delta Eme.path_loss_db(144.0, d), 252.0, 1.0
      assert_in_delta Eme.path_loss_db(432.0, d), 261.5, 1.0
      assert_in_delta Eme.path_loss_db(1_296.0, d), 271.0, 1.0
      assert_in_delta Eme.path_loss_db(10_368.0, d), 289.1, 1.0
    end

    test "monotonic in frequency at fixed distance" do
      for d <- [356_500.0, 384_400.0, 406_700.0] do
        freqs = [50.0, 144.0, 432.0, 1_296.0, 2_304.0, 5_760.0, 10_368.0, 24_048.0]
        losses = Enum.map(freqs, &Eme.path_loss_db(&1, d))
        assert losses == Enum.sort(losses)
      end
    end

    test "doubling distance adds 12 dB (4·log2(2) × 10·log10 ≈ 12.04)" do
      for {f, d} <- [{432.0, 200_000.0}, {1_296.0, 300_000.0}, {10_368.0, 400_000.0}] do
        base = Eme.path_loss_db(f, d)
        doubled = Eme.path_loss_db(f, d * 2)
        assert_in_delta doubled - base, 12.04, 0.01
      end
    end

    test "doubling frequency adds 6 dB (20·log10 of 2)" do
      for {f, d} <- [{144.0, 384_400.0}, {432.0, 384_400.0}, {1_296.0, 360_000.0}] do
        base = Eme.path_loss_db(f, d)
        doubled = Eme.path_loss_db(f * 2, d)
        assert_in_delta doubled - base, 6.02, 0.01
      end
    end

    property "always increases with distance at fixed frequency" do
      check all(
              f <- float(min: 10.0, max: 100_000.0),
              d1 <- float(min: 300_000.0, max: 400_000.0),
              delta <- float(min: 1.0, max: 200_000.0)
            ) do
        d2 = d1 + delta
        assert Eme.path_loss_db(f, d2) > Eme.path_loss_db(f, d1)
      end
    end

    property "always increases with frequency at fixed distance" do
      check all(
              d <- float(min: 356_500.0, max: 406_700.0),
              f1 <- float(min: 10.0, max: 10_000.0),
              delta <- float(min: 1.0, max: 10_000.0)
            ) do
        f2 = f1 + delta
        assert Eme.path_loss_db(f2, d) > Eme.path_loss_db(f1, d)
      end
    end
  end

  describe "moon_rcs_m2/1" do
    test "agrees with the σ = 0.065 · π · R² closed form within 1 % (default albedo)" do
      # R_moon ≈ 1738.1 km, ρ = 0.065 → σ ≈ 6.16 × 10¹¹ m².
      rcs = Eme.moon_rcs_m2()
      assert_in_delta rcs, 6.16e11, 0.01 * 6.16e11
    end

    test "scales linearly with the albedo override" do
      assert_in_delta Eme.moon_rcs_m2(0.13) / Eme.moon_rcs_m2(0.065), 2.0, 1.0e-9
      assert_in_delta Eme.moon_rcs_m2(0.0325) / Eme.moon_rcs_m2(0.065), 0.5, 1.0e-9
    end
  end

  describe "moon_albedo/1 band lookup" do
    test "returns the classic 0.065 baseline at every configured VHF band" do
      for f <- [50.0, 144.0, 222.0] do
        assert Eme.moon_albedo(f) == 0.065
      end
    end

    test "peaks around 10 GHz (≈ 0.090) and falls off above 40 GHz" do
      assert Eme.moon_albedo(10_368.0) == 0.090
      assert Eme.moon_albedo(5_760.0) == 0.080
      # >= 76 GHz is the terminal "thermal-emission dominates" regime.
      assert Eme.moon_albedo(76_000.0) == 0.020
      assert Eme.moon_albedo(241_000.0) == 0.020
    end

    test "every amateur microwave band in BandConfig gets a positive albedo" do
      for {_label, mhz_str} <- BandConfig.band_options() do
        {mhz, _} = Integer.parse(mhz_str)
        rho = Eme.moon_albedo(mhz)
        assert is_number(rho) and rho > 0.0 and rho < 0.2
      end
    end
  end

  describe "fspl_round_trip_db + moon_reflection_gain_db decomposition" do
    test "sum equals path_loss_db at the same (f, d, ρ) within float epsilon" do
      # Pick a variety of band / albedo pairs. The decomposition
      # identity holds algebraically so the tolerance is tiny.
      for {f, d} <- [{144.0, 384_400.0}, {1_296.0, 360_000.0}, {10_368.0, 400_000.0}, {24_048.0, 356_500.0}] do
        rho = Eme.moon_albedo(f)

        expected = Eme.path_loss_db(f, d, rho)
        components = Eme.fspl_round_trip_db(f, d) - Eme.moon_reflection_gain_db(f, rho)

        assert_in_delta components, expected, 1.0e-9
      end
    end

    test "moon_reflection_gain_db is positive at every amateur band (gain reduces loss)" do
      for {_label, mhz_str} <- BandConfig.band_options() do
        {mhz, _} = Integer.parse(mhz_str)
        rho = Eme.moon_albedo(mhz)
        gain = Eme.moon_reflection_gain_db(mhz, rho)
        assert gain > 0.0, "expected positive moon reflection gain at #{mhz} MHz, got #{gain}"
      end
    end

    test "doubling albedo adds exactly 3.01 dB to the moon-reflection gain" do
      for f <- [144.0, 1_296.0, 10_368.0] do
        g1 = Eme.moon_reflection_gain_db(f, 0.05)
        g2 = Eme.moon_reflection_gain_db(f, 0.10)
        assert_in_delta g2 - g1, 3.01, 0.01
      end
    end
  end

  describe "path_loss_db/3 with band-aware albedo" do
    test "uses the supplied albedo — different ρ gives different loss" do
      flat = Eme.path_loss_db(10_368.0, 384_400.0)
      band_aware = Eme.path_loss_db(10_368.0, 384_400.0, Eme.moon_albedo(10_368.0))

      # At 10 GHz band_aware uses ρ = 0.090 vs flat ρ = 0.065 →
      # band_aware should be ~1.4 dB LOWER (less loss because the
      # moon reflects more at cm-wavelengths).
      assert band_aware < flat
      assert_in_delta flat - band_aware, 1.4, 0.2
    end
  end

  describe "received_power_dbm/1" do
    test "classic 1296 MHz / 1 kW / 45 dBi case lands at ~ -145 dBm" do
      # 30 dBm + 45 dBi + 45 dBi - 271 dB ≈ -151 dBm (weak but
      # workable for a CW operator with a cooled LNA). 1 kW = 60 dBm.
      p =
        Eme.received_power_dbm(
          tx_power_dbm: 60.0,
          tx_gain_dbi: 45.0,
          rx_gain_dbi: 45.0,
          freq_mhz: 1_296.0,
          distance_km: 384_400.0
        )

      assert_in_delta p, -121.0, 1.0
    end

    test "every dB added to gain adds 1 dB to received power (linearity)" do
      base =
        Eme.received_power_dbm(
          tx_power_dbm: 50.0,
          tx_gain_dbi: 30.0,
          rx_gain_dbi: 30.0,
          freq_mhz: 1_296.0,
          distance_km: 384_400.0
        )

      bumped =
        Eme.received_power_dbm(
          tx_power_dbm: 50.0,
          tx_gain_dbi: 31.0,
          rx_gain_dbi: 30.0,
          freq_mhz: 1_296.0,
          distance_km: 384_400.0
        )

      assert_in_delta bumped - base, 1.0, 0.001
    end
  end

  describe "doppler_shift_hz/2" do
    test "zero radial velocity gives zero shift" do
      assert Eme.doppler_shift_hz(1_296.0, 0.0) == 0.0
    end

    test "receding Moon (+v) produces a negative (down-shifted) return" do
      # +1 m/s = +0.001 km/s receding at 1296 MHz →
      # Δf = -2 * 1296e6 * 1 / 3e8 ≈ -8.65 Hz.
      dop = Eme.doppler_shift_hz(1_296.0, 0.001)
      assert dop < 0.0
      assert_in_delta dop, -8.65, 0.05
    end

    test "approaching Moon (−v) produces a positive (up-shifted) return" do
      dop = Eme.doppler_shift_hz(10_368.0, -0.5)
      assert dop > 0.0
      # Δf = +2 * 10.368e9 * 500 / 3e8 ≈ +34_560 Hz.
      assert_in_delta dop, 34_560.0, 100.0
    end

    test "scales linearly with frequency at fixed radial velocity" do
      dop_1296 = Eme.doppler_shift_hz(1_296.0, 0.1)
      dop_10368 = Eme.doppler_shift_hz(10_368.0, 0.1)
      assert_in_delta dop_10368 / dop_1296, 10_368.0 / 1_296.0, 1.0e-9
    end

    test "scales linearly with radial velocity at fixed frequency" do
      a = Eme.doppler_shift_hz(1_296.0, 0.2)
      b = Eme.doppler_shift_hz(1_296.0, 0.4)
      assert_in_delta b / a, 2.0, 1.0e-9
    end

    property "sign is always opposite to radial_velocity (receding → down-shift)" do
      check all(
              f <- float(min: 50.0, max: 100_000.0),
              v <- float(min: -2.0, max: 2.0),
              v != 0.0
            ) do
        dop = Eme.doppler_shift_hz(f, v)

        if v > 0 do
          assert dop < 0
        else
          assert dop > 0
        end
      end
    end
  end

  describe "noise_floor_dbm/2" do
    test "-174 dBm/Hz at room temperature (290 K) matches kTB textbook" do
      assert_in_delta Eme.noise_floor_dbm(1.0, 290.0), -174.0, 0.01
    end

    test "+10 dB noise increase per decade of bandwidth" do
      base = Eme.noise_floor_dbm(100.0, 290.0)
      decade = Eme.noise_floor_dbm(1_000.0, 290.0)
      assert_in_delta decade - base, 10.0, 0.01
    end

    test "halving Tsys drops noise floor by ~3 dB" do
      hot = Eme.noise_floor_dbm(2500.0, 290.0)
      cold = Eme.noise_floor_dbm(2500.0, 145.0)
      assert_in_delta hot - cold, 3.01, 0.01
    end
  end

  describe "snr_db/1" do
    test "own-echo CW SNR on a decent 1296 MHz station comes out positive" do
      # 500 W (57 dBm), 3 m dish (~30 dBi), 100 Hz CW bandwidth, 100 K
      # system noise. Should be ~12 dB above thermal — plenty for CW.
      snr =
        Eme.snr_db(
          tx_power_dbm: 57.0,
          tx_gain_dbi: 32.0,
          rx_gain_dbi: 32.0,
          freq_mhz: 1_296.0,
          distance_km: 384_400.0,
          bandwidth_hz: 100.0,
          t_sys_k: 100.0
        )

      assert snr > 5.0 and snr < 30.0
    end

    test "narrowing detection bandwidth improves SNR one-for-one in dB" do
      opts = [
        tx_power_dbm: 50.0,
        tx_gain_dbi: 30.0,
        rx_gain_dbi: 30.0,
        freq_mhz: 1_296.0,
        distance_km: 384_400.0,
        t_sys_k: 290.0
      ]

      snr_wide = Eme.snr_db(Keyword.put(opts, :bandwidth_hz, 2500.0))
      snr_narrow = Eme.snr_db(Keyword.put(opts, :bandwidth_hz, 25.0))

      # 100× narrower → 20 dB less noise → 20 dB more SNR.
      assert_in_delta snr_narrow - snr_wide, 20.0, 0.01
    end
  end
end
