defmodule Microwaveprop.Weather.SoundingParamsPropertyTest do
  @moduledoc """
  Property tests for `Microwaveprop.Weather.SoundingParams` — exercises
  the `derive/1` invariants on arbitrary (but physically-plausible)
  sounding profiles.
  """

  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Microwaveprop.Weather.SoundingParams

  # ── Generators ──────────────────────────────────────────────────

  # A realistic mandatory-level pressure sequence: surface down to the
  # upper troposphere. Pressures strictly decrease. We build it by
  # walking down from a surface pressure, sampling a positive
  # step between each level — cheaper than `uniq_list_of` at small
  # generation sizes and guaranteed to avoid duplicates.
  defp pressure_levels_gen(n) when n >= 0 do
    gen all(
          surface <- float(min: 900.0, max: 1020.0),
          steps <- list_of(float(min: 5.0, max: 60.0), length: max(n - 1, 0))
        ) do
      {levels, _} =
        Enum.reduce(steps, {[surface], surface}, fn step, {acc, prev} ->
          next = prev - step
          {[next | acc], next}
        end)

      levels
      |> Enum.reverse()
      |> Enum.take(n)
    end
  end

  # A single level at a given pressure/height with temperature and
  # dewpoint generated inside the level. `dwpc` is forced ≤ `tmpc` so
  # the depression is physical.
  defp level_gen(pres, hght) do
    gen all(
          tmpc <- float(min: -80.0, max: 40.0),
          dep <- float(min: 0.0, max: 40.0)
        ) do
      %{
        "pres" => pres,
        "tmpc" => tmpc,
        "dwpc" => tmpc - dep,
        "hght" => hght,
        "drct" => 0.0,
        "sknt" => 0.0
      }
    end
  end

  # Profile generator: heights increase strictly as pressure decreases
  # (the hydrostatic atmospheric convention). Built by pairing the
  # sorted pressure sequence (desc) with a heights sequence (asc,
  # starting at surface elevation), then generating temps/dewpoints per
  # level.
  defp profile_gen(min_levels, max_levels) do
    gen all(
          n <- integer(min_levels..max_levels),
          pressures <- pressure_levels_gen(n),
          surface_hght <- float(min: 0.0, max: 3000.0),
          height_steps <- list_of(float(min: 50.0, max: 1500.0), length: max(n - 1, 0)),
          levels <-
            surface_hght
            |> heights_from_surface(height_steps, n)
            |> Enum.zip(pressures)
            |> Enum.map(fn {h, p} -> level_gen(p, h) end)
            |> fixed_list()
        ) do
      levels
    end
  end

  defp heights_from_surface(surface, steps, n) do
    {hs, _} =
      Enum.reduce(steps, {[surface], surface}, fn step, {acc, prev} ->
        next = prev + step
        {[next | acc], next}
      end)

    hs |> Enum.reverse() |> Enum.take(n)
  end

  # ── derive/1 invariants ────────────────────────────────────────

  property "derive/1 returns a result map whenever ≥3 valid levels are given" do
    check all(profile <- profile_gen(3, 12)) do
      assert %{} = result = SoundingParams.derive(profile)
      assert result.level_count >= 3
      assert result.level_count == length(profile)
      assert is_list(result.profile)
      assert is_list(result.inversions)
      assert is_boolean(result.ducting_detected)
    end
  end

  property "derive/1 returns nil when fewer than 3 valid levels are provided" do
    check all(
            n <- integer(0..2),
            profile <- short_profile_gen(n)
          ) do
      assert SoundingParams.derive(profile) == nil
    end
  end

  defp short_profile_gen(n) do
    gen all(
          pressures <- pressure_levels_gen(n),
          surface_hght <- float(min: 0.0, max: 3000.0),
          height_steps <- list_of(float(min: 50.0, max: 1500.0), length: max(n - 1, 0)),
          levels <-
            surface_hght
            |> heights_from_surface(height_steps, n)
            |> Enum.zip(pressures)
            |> Enum.map(fn {h, p} -> level_gen(p, h) end)
            |> fixed_list()
        ) do
      levels
    end
  end

  property "precipitable_water_mm is non-negative when present" do
    check all(profile <- profile_gen(4, 10)) do
      %{precipitable_water_mm: pw} = SoundingParams.derive(profile)
      assert is_number(pw)
      assert pw >= 0.0
    end
  end

  property "boundary_layer_depth_m is non-negative when it resolves" do
    check all(profile <- profile_gen(4, 10)) do
      case SoundingParams.derive(profile).boundary_layer_depth_m do
        nil -> :ok
        depth -> assert depth >= 0.0
      end
    end
  end

  property "ducting_detected agrees with duct_characteristics non-nilness" do
    check all(profile <- profile_gen(4, 12)) do
      %{ducting_detected: detected, duct_characteristics: chars} = SoundingParams.derive(profile)
      # nil chars ⇔ no ducts detected
      assert detected == (chars != nil)
    end
  end

  property "derive/1 filters out levels missing pres/tmpc/hght before the count check" do
    check all(
            bad_level_count <- integer(0..3),
            good_profile <- profile_gen(3, 6)
          ) do
      bad_levels =
        List.duplicate(%{"pres" => nil, "tmpc" => nil, "hght" => nil, "dwpc" => 0.0}, bad_level_count)

      merged = bad_levels ++ good_profile
      # Good levels alone satisfy the ≥3 threshold, so adding garbage
      # should not push the result to nil.
      assert SoundingParams.derive(merged)
    end
  end

  # ── sat_vap_pres / mixing_ratio helpers ─────────────────────────

  property "sat_vap_pres is positive and monotonically increasing in temperature" do
    check all(
            t_lo <- float(min: -40.0, max: 30.0),
            delta <- float(min: 0.1, max: 20.0)
          ) do
      t_hi = t_lo + delta
      e_lo = SoundingParams.sat_vap_pres(t_lo)
      e_hi = SoundingParams.sat_vap_pres(t_hi)

      assert e_lo > 0.0
      assert e_hi > 0.0
      assert e_hi > e_lo
    end
  end

  property "surface_refractivity is non-negative for every derivable profile" do
    check all(profile <- profile_gen(3, 10)) do
      %{surface_refractivity: n} = SoundingParams.derive(profile)
      # N may be nil if surface level lacked dewpoint (our generator
      # always supplies one), otherwise it's the P.453 N value which is
      # strictly positive for positive P, T, e.
      case n do
        nil -> :ok
        value -> assert value >= 0.0
      end
    end
  end

  property "mixing_ratio is positive for temperatures well below pressure" do
    # The formula hits a pole when saturation vapour pressure approaches
    # the total pressure, which only happens at boiling-hot temperatures
    # relative to pressure. Realistic weather stays comfortably away.
    check all(
            t_c <- float(min: -60.0, max: 30.0),
            p_mb <- float(min: 300.0, max: 1020.0)
          ) do
      mr = SoundingParams.mixing_ratio(t_c, p_mb)
      assert is_number(mr)
      assert mr > 0.0
    end
  end
end
