defmodule Microwaveprop.Propagation.DuctPropertyTest do
  @moduledoc """
  StreamData property tests for the pure-math core of
  `Microwaveprop.Propagation.Duct`: refractivity/M-profile
  transformations, `detect_ducts/1`, and the trapped-frequency
  approximation from Bean & Dutton (1966).

  Each property encodes one physical invariant (positive trapped
  frequency for well-formed ducts, monotonicity in thickness, identity
  between N and M-profile lengths, empty-duct list for monotonic
  M-profiles).
  """

  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Microwaveprop.Propagation.Duct

  describe "min_trapped_frequency_ghz/1" do
    property "is strictly positive for any positive thickness and deficit" do
      check all(
              d <- float(min: 0.5, max: 2_000.0),
              delta <- float(min: 0.01, max: 200.0)
            ) do
        f = Duct.min_trapped_frequency_ghz(%{thickness_m: d, m_deficit: delta})
        assert f > 0
        assert is_float(f)
      end
    end

    property "thicker ducts trap lower minimum frequencies (at fixed deficit)" do
      # f_min = c / (2.5 * d * sqrt(ΔM * 1e-6)) — strictly decreasing in d.
      check all(
              delta <- float(min: 0.5, max: 50.0),
              d_small <- float(min: 1.0, max: 100.0),
              bump <- float(min: 1.0, max: 500.0)
            ) do
        f_small = Duct.min_trapped_frequency_ghz(%{thickness_m: d_small, m_deficit: delta})
        f_big = Duct.min_trapped_frequency_ghz(%{thickness_m: d_small + bump, m_deficit: delta})
        assert f_big < f_small
      end
    end

    property "degenerate inputs (zero or negative thickness/deficit) return the 999 GHz sentinel" do
      check all(
              d <- float(min: -10.0, max: 0.0),
              delta <- float(min: -10.0, max: 10.0)
            ) do
        assert Duct.min_trapped_frequency_ghz(%{thickness_m: d, m_deficit: delta}) == 999.0
      end

      check all(
              d <- float(min: 1.0, max: 100.0),
              delta <- float(min: -10.0, max: 0.0)
            ) do
        assert Duct.min_trapped_frequency_ghz(%{thickness_m: d, m_deficit: delta}) == 999.0
      end
    end
  end

  describe "refractivity_profile/1 & m_profile/1" do
    property "output length matches input level count" do
      check all(levels <- list_of(level_gen(), min_length: 1, max_length: 20)) do
        sorted = Enum.sort_by(levels, & &1.h)

        profile = %{
          heights_m: Enum.map(sorted, & &1.h),
          temp_k: Enum.map(sorted, & &1.t),
          spfh: Enum.map(sorted, & &1.q),
          pressure_pa: Enum.map(sorted, & &1.p)
        }

        n_profile = Duct.refractivity_profile(profile)
        assert length(n_profile) == length(sorted)

        m_out = Duct.m_profile(n_profile)
        assert length(m_out) == length(sorted)

        # All N values positive for any physical atmosphere.
        for {_h, n} <- n_profile, do: assert(n > 0)
      end
    end
  end

  describe "detect_ducts/1" do
    property "returns [] for any strictly increasing M-profile (standard atmosphere)" do
      check all(
              heights <- list_of(float(min: 0.0, max: 10_000.0), min_length: 2, max_length: 15),
              base_m <- float(min: 200.0, max: 400.0),
              slope <- float(min: 0.1, max: 0.5)
            ) do
        sorted_h = heights |> Enum.uniq() |> Enum.sort()

        # Skip degenerate cases where uniq removed too many entries.
        if length(sorted_h) >= 2 do
          m_profile = Enum.map(sorted_h, fn h -> {h, base_m + slope * h} end)
          assert Duct.detect_ducts(m_profile) == []
        end
      end
    end

    property "profiles with fewer than 2 points always return []" do
      check all(n_opt <- integer(0..1)) do
        prof =
          case n_opt do
            0 -> []
            1 -> [{0.0, 350.0}]
          end

        assert Duct.detect_ducts(prof) == []
      end
    end
  end

  # ── helpers ────────────────────────────────────────────────────

  defp level_gen do
    gen all(
          h <- float(min: 0.0, max: 10_000.0),
          t <- float(min: 220.0, max: 320.0),
          q <- float(min: 0.0001, max: 0.025),
          p <- float(min: 20_000.0, max: 102_000.0)
        ) do
      %{h: h, t: t, q: q, p: p}
    end
  end
end
