defmodule Microwaveprop.Weather.GefsClientTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Weather.GefsClient

  describe "nearest_run/1" do
    test "rounds down to the same 6-hour cycle within the cycle window" do
      assert GefsClient.nearest_run(~U[2026-04-18 13:30:00Z]) == ~U[2026-04-18 12:00:00Z]
    end

    test "snaps to 00Z when given 02:15" do
      assert GefsClient.nearest_run(~U[2026-04-18 02:15:00Z]) == ~U[2026-04-18 00:00:00Z]
    end

    test "snaps to 18Z when given 23:59" do
      assert GefsClient.nearest_run(~U[2026-04-18 23:59:00Z]) == ~U[2026-04-18 18:00:00Z]
    end

    test "is a no-op when already on a cycle boundary" do
      assert GefsClient.nearest_run(~U[2026-04-18 06:00:00Z]) == ~U[2026-04-18 06:00:00Z]
    end
  end

  describe "ensmean_url/3" do
    test "builds a pgrb2ap5 ensemble-mean URL for a run + forecast hour" do
      url = GefsClient.ensmean_url(~D[2026-04-18], 12, 24)

      assert url ==
               "https://nomads.ncep.noaa.gov/pub/data/nccf/com/gens/prod/gefs.20260418/12/atmos/pgrb2ap5/geavg.t12z.pgrb2a.0p50.f024"
    end

    test "pads forecast hour to three digits" do
      url = GefsClient.ensmean_url(~D[2026-04-18], 0, 6)
      assert String.ends_with?(url, "geavg.t00z.pgrb2a.0p50.f006")
    end

    test "handles 240-hour forecast horizon" do
      url = GefsClient.ensmean_url(~D[2026-04-18], 12, 240)
      assert String.ends_with?(url, "geavg.t12z.pgrb2a.0p50.f240")
    end

    test "pads single-digit runs" do
      url = GefsClient.ensmean_url(~D[2026-04-18], 6, 24)
      assert String.contains?(url, "/06/atmos/")
      assert String.contains?(url, "geavg.t06z.")
    end
  end

  describe "idx_url/1" do
    test "appends .idx to the GRIB2 URL" do
      base = GefsClient.ensmean_url(~D[2026-04-18], 12, 24)
      assert GefsClient.idx_url(base) == base <> ".idx"
    end
  end

  describe "forecast_hours/0" do
    test "returns 3-hourly hours out to 240 then 6-hourly to 384" do
      hours = GefsClient.forecast_hours()
      assert Enum.take(hours, 4) == [0, 3, 6, 9]
      assert 240 in hours
      assert 246 in hours
      assert 384 in hours
      assert List.last(hours) == 384
      refute 243 in hours
    end
  end

  describe "surface_messages/0" do
    test "lists the variables the scorer needs from the pgrb2a file" do
      msgs = GefsClient.surface_messages()

      vars = Enum.map(msgs, & &1.var)
      assert "TMP" in vars
      assert "RH" in vars
      assert "PRES" in vars
      assert "PWAT" in vars
      assert "UGRD" in vars
      assert "VGRD" in vars
      assert "TCDC" in vars
      assert "APCP" in vars
    end

    test "2m TMP and RH use the '2 m above ground' level string" do
      msgs = GefsClient.surface_messages()

      assert %{var: "TMP", level: "2 m above ground"} in msgs
      assert %{var: "RH", level: "2 m above ground"} in msgs
    end

    test "10m winds use the '10 m above ground' level string" do
      msgs = GefsClient.surface_messages()

      assert %{var: "UGRD", level: "10 m above ground"} in msgs
      assert %{var: "VGRD", level: "10 m above ground"} in msgs
    end
  end

  describe "build_profile/1" do
    @raw %{
      "TMP:2 m above ground" => 293.15,
      "RH:2 m above ground" => 50.0,
      "PRES:surface" => 101_325.0,
      "PWAT:entire atmosphere (considered as a single layer)" => 25.4,
      "UGRD:10 m above ground" => 3.0,
      "VGRD:10 m above ground" => -1.5,
      "TCDC:entire atmosphere" => 62.0,
      "APCP:surface" => 0.0,
      "TMP:1000 mb" => 293.15,
      "RH:1000 mb" => 50.0,
      "HGT:1000 mb" => 110.0,
      "TMP:925 mb" => 288.15,
      "RH:925 mb" => 40.0,
      "HGT:925 mb" => 780.0
    }

    test "converts surface TMP from Kelvin to Celsius" do
      p = GefsClient.build_profile(@raw)
      assert_in_delta p.surface_temp_c, 20.0, 0.01
    end

    test "converts surface PRES from Pa to mb" do
      p = GefsClient.build_profile(@raw)
      assert_in_delta p.surface_pressure_mb, 1013.25, 0.1
    end

    test "derives surface dewpoint from RH via Magnus" do
      p = GefsClient.build_profile(@raw)
      assert_in_delta p.surface_dewpoint_c, 9.27, 0.3
    end

    test "carries PWAT, winds, cloud cover, precip through untouched" do
      p = GefsClient.build_profile(@raw)
      assert p.pwat_mm == 25.4
      assert p.wind_u == 3.0
      assert p.wind_v == -1.5
      assert p.cloud_cover_pct == 62.0
      assert p.precip_mm == 0.0
    end

    test "builds a pressure-level profile with Td derived from RH at each level" do
      p = GefsClient.build_profile(@raw)
      assert length(p.profile) == 2

      l1000 = Enum.find(p.profile, &(&1["pres"] == 1000.0))
      assert_in_delta l1000["tmpc"], 20.0, 0.01
      assert_in_delta l1000["dwpc"], 9.27, 0.3
      assert l1000["hght"] == 110.0

      l925 = Enum.find(p.profile, &(&1["pres"] == 925.0))
      assert_in_delta l925["tmpc"], 15.0, 0.01
      assert l925["dwpc"] < l925["tmpc"]
    end

    test "falls back to the lowest pressure-level values when surface fields are missing" do
      raw = %{
        "TMP:1000 mb" => 293.15,
        "RH:1000 mb" => 60.0,
        "HGT:1000 mb" => 100.0
      }

      p = GefsClient.build_profile(raw)
      assert_in_delta p.surface_temp_c, 20.0, 0.01
      assert is_float(p.surface_dewpoint_c)
      assert p.surface_pressure_mb == nil
    end

    test "skips pressure levels that lack TMP or HGT" do
      raw =
        Map.drop(@raw, [
          "TMP:925 mb",
          "RH:925 mb",
          "HGT:925 mb"
        ])

      p = GefsClient.build_profile(raw)
      pressures = Enum.map(p.profile, & &1["pres"])
      assert 1000.0 in pressures
      refute 925.0 in pressures
    end
  end

  describe "dewpoint_from_rh/2" do
    test "returns the input temperature when RH is 100%" do
      # At saturation, Td == T. Magnus returns T exactly.
      assert_in_delta GefsClient.dewpoint_from_rh(20.0, 100.0), 20.0, 0.05
    end

    test "returns a cooler dewpoint than temperature when RH is below saturation" do
      td = GefsClient.dewpoint_from_rh(25.0, 50.0)
      assert td < 25.0
      # Tables put Td(25 °C, 50% RH) at ~13.9 °C. Allow ±0.5 °C slack.
      assert_in_delta td, 13.9, 0.5
    end

    test "handles very dry air without crashing" do
      td = GefsClient.dewpoint_from_rh(30.0, 5.0)
      assert is_float(td)
      assert td < 0.0
    end

    test "clamps RH<=0 to a very-dry fallback rather than returning NaN/Inf" do
      td = GefsClient.dewpoint_from_rh(10.0, 0.0)
      assert is_float(td)
      assert td < -30.0
    end
  end
end
