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

  alias Microwaveprop.Propagation.MechanismClassifier

  defp inputs(overrides \\ %{}) do
    # Base: a plain 10 GHz tropo-scatter contact with everything nil.
    base = %{
      band_mhz: 10_000,
      distance_km: 250.0,
      qso_timestamp: ~U[2024-08-15 19:00:00Z],
      pos1: %{"lat" => 32.9, "lon" => -97.0},
      pos2: %{"lat" => 30.3, "lon" => -97.7},
      user_declared_prop_mode: nil,
      radar: nil,
      duct_either_endpoint: false,
      native_best_duct_ghz: nil,
      kp_index: nil,
      foes_mhz: nil,
      active_meteor_shower: nil
    }

    Map.merge(base, overrides)
  end

  describe "user-declared mechanism (ADIF PROP_MODE) — highest priority" do
    test "trusts ADIF 'EME' even on 10 GHz short path" do
      result = MechanismClassifier.classify(inputs(%{user_declared_prop_mode: "EME"}))
      assert result.mechanism == :eme
      assert result.confidence == :high
    end

    test "trusts ADIF 'ES' even when ionosonde data absent" do
      result = MechanismClassifier.classify(inputs(%{band_mhz: 144, user_declared_prop_mode: "ES"}))
      assert result.mechanism == :sporadic_e
      assert result.confidence == :high
    end

    test "trusts ADIF 'MS' for meteor scatter" do
      result = MechanismClassifier.classify(inputs(%{band_mhz: 144, user_declared_prop_mode: "MS"}))
      assert result.mechanism == :meteor_scatter
    end

    test "trusts ADIF 'RS' for rain scatter" do
      result = MechanismClassifier.classify(inputs(%{user_declared_prop_mode: "RS"}))
      assert result.mechanism == :rain_scatter
    end

    test "trusts ADIF 'AS' for aircraft scatter" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 1_296, user_declared_prop_mode: "AS"}))

      assert result.mechanism == :aircraft_scatter
    end

    test "unknown user-declared mode falls through to physics classification" do
      result =
        MechanismClassifier.classify(inputs(%{user_declared_prop_mode: "GIBBERISH"}))

      # No rain, no duct, no user hint → default to troposcatter
      assert result.mechanism == :troposcatter
    end
  end

  describe "EME detection (calc-only)" do
    test "2m 5000 km path with moon visible at both ends -> :eme" do
      # A 5000 km path on 2m at a time when the moon can be visible at both
      # ends is basically always EME.
      result =
        MechanismClassifier.classify(
          inputs(%{
            band_mhz: 144,
            distance_km: 5_000.0,
            # A time chosen so the moon is above both stations — handled by
            # MoonEphemeris inside the classifier; test just asserts the
            # classification flows through when distance/band say "EME"
            qso_timestamp: ~U[2024-08-15 04:00:00Z]
          })
        )

      assert result.mechanism == :eme
    end

    test "short 300 km 2m path -> NOT EME" do
      result = MechanismClassifier.classify(inputs(%{band_mhz: 144, distance_km: 300.0}))
      refute result.mechanism == :eme
    end

    test "6 m (50 MHz) is below EME's practical band range -> NOT EME" do
      result = MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 5_000.0}))
      refute result.mechanism == :eme
    end
  end

  describe "sporadic-E (ionosonde-driven)" do
    test "6 m 1500 km path, foEs=12 MHz (MUF=60) -> :sporadic_e" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 1_500.0, foes_mhz: 12.0}))

      assert result.mechanism == :sporadic_e
    end

    test "2 m 1500 km path, foEs=30 MHz (MUF=150) -> :sporadic_e (extraordinary event)" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 144, distance_km: 1_500.0, foes_mhz: 30.0}))

      assert result.mechanism == :sporadic_e
    end

    test "6 m 1500 km with foEs=8 MHz (MUF=40 < 50) -> NOT Es (MUF too low)" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 1_500.0, foes_mhz: 8.0}))

      refute result.mechanism == :sporadic_e
    end

    test "6 m short 200 km path -> NOT Es (below single-hop Es minimum)" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 200.0, foes_mhz: 8.0}))

      refute result.mechanism == :sporadic_e
    end

    test "6 m 1500 km with weak foEs=3 MHz -> NOT Es (MUF too low for 6m)" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 1_500.0, foes_mhz: 3.0}))

      refute result.mechanism == :sporadic_e
    end
  end

  describe "aurora (Kp-driven + high-lat path)" do
    test "2 m 500 km N-S path, Kp=7 -> :aurora" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            band_mhz: 144,
            distance_km: 500.0,
            pos1: %{"lat" => 45.0, "lon" => -95.0},
            pos2: %{"lat" => 49.0, "lon" => -95.0},
            kp_index: 7
          })
        )

      assert result.mechanism == :aurora
    end

    test "10 GHz microwave is far above typical aurora band range -> NOT aurora" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            band_mhz: 10_000,
            kp_index: 8,
            pos1: %{"lat" => 45.0, "lon" => -95.0},
            pos2: %{"lat" => 49.0, "lon" => -95.0}
          })
        )

      refute result.mechanism == :aurora
    end

    test "quiet Kp=2 on VHF -> NOT aurora even on a polar path" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            band_mhz: 144,
            kp_index: 2,
            pos1: %{"lat" => 45.0, "lon" => -95.0},
            pos2: %{"lat" => 49.0, "lon" => -95.0}
          })
        )

      refute result.mechanism == :aurora
    end
  end

  describe "meteor scatter" do
    test "6 m 900 km path during Perseids -> :meteor_scatter (no other mechanism fires)" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            band_mhz: 50,
            distance_km: 900.0,
            qso_timestamp: ~U[2024-08-12 10:00:00Z],
            active_meteor_shower: "Perseids"
          })
        )

      assert result.mechanism == :meteor_scatter
    end

    test "no active shower -> NOT meteor_scatter" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 50, distance_km: 900.0, active_meteor_shower: nil}))

      refute result.mechanism == :meteor_scatter
    end

    test "10 GHz is far above practical meteor scatter -> NOT meteor_scatter" do
      result =
        MechanismClassifier.classify(inputs(%{band_mhz: 10_000, active_meteor_shower: "Geminids"}))

      refute result.mechanism == :meteor_scatter
    end
  end

  describe "rain scatter (delegates to existing logic)" do
    test "10 GHz 250 km, heavy rain in CV, no duct -> :rain_scatter" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            radar: %{max_dbz: 42.0, heavy_rain_pixel_count: 12, coverage_pct: 80.0}
          })
        )

      assert result.mechanism == :rain_scatter
    end

    test "light rain (25-35 dBZ) -> :rain_scatter_possible" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            radar: %{max_dbz: 28.0, heavy_rain_pixel_count: 0, coverage_pct: 80.0}
          })
        )

      assert result.mechanism == :rain_scatter_possible
    end
  end

  describe "tropo duct (HRRR native-profile best duct ≥ target band)" do
    test "10 GHz with native_best_duct_ghz=24 -> :tropo_duct (duct supports target)" do
      result =
        MechanismClassifier.classify(inputs(%{native_best_duct_ghz: 24.0}))

      assert result.mechanism == :tropo_duct
    end

    test "10 GHz with native_best_duct_ghz=5 -> does NOT classify as duct at 10 GHz" do
      # The sub-10-GHz duct doesn't support 10 GHz propagation.
      result = MechanismClassifier.classify(inputs(%{native_best_duct_ghz: 5.0}))
      refute result.mechanism == :tropo_duct
    end

    test "legacy flag duct_either_endpoint=true still classifies as duct" do
      result = MechanismClassifier.classify(inputs(%{duct_either_endpoint: true}))
      assert result.mechanism == :tropo_duct
    end
  end

  describe "line of sight" do
    test "very short 5 km path -> :line_of_sight" do
      result = MechanismClassifier.classify(inputs(%{distance_km: 5.0}))
      assert result.mechanism == :line_of_sight
    end

    test "normal 250 km path -> NOT line_of_sight" do
      result = MechanismClassifier.classify(inputs())
      refute result.mechanism == :line_of_sight
    end
  end

  describe "troposcatter (default / fallback)" do
    test "10 GHz 250 km, no rain, no duct, no hint -> :troposcatter" do
      result = MechanismClassifier.classify(inputs())
      assert result.mechanism == :troposcatter
    end
  end

  describe "confidence levels" do
    test "user-declared -> :high" do
      result = MechanismClassifier.classify(inputs(%{user_declared_prop_mode: "RS"}))
      assert result.confidence == :high
    end

    test "strong physics signal -> :medium" do
      result =
        MechanismClassifier.classify(
          inputs(%{
            radar: %{max_dbz: 48.0, heavy_rain_pixel_count: 20, coverage_pct: 85.0}
          })
        )

      assert result.confidence in [:high, :medium]
    end

    test "default troposcatter fallback -> :low" do
      result = MechanismClassifier.classify(inputs())
      assert result.confidence == :low
    end
  end
end
