defmodule Microwaveprop.WeatherTest do
  use Microwaveprop.DataCase, async: true

  alias Microwaveprop.Weather
  alias Microwaveprop.Weather.HrrrNativeProfile
  alias Microwaveprop.Weather.HrrrProfile
  alias Microwaveprop.Weather.IemreObservation

  @station_attrs %{
    station_code: "KDFW",
    station_type: "asos",
    name: "Dallas/Fort Worth International",
    lat: 32.8998,
    lon: -97.0403,
    elevation_m: 171.0,
    state: "TX"
  }

  @sounding_station_attrs %{
    station_code: "FWD",
    station_type: "sounding",
    wmo_number: 72_249,
    name: "Fort Worth",
    lat: 32.83,
    lon: -97.30
  }

  describe "find_or_create_station/1" do
    test "creates a new station" do
      assert {:ok, station} = Weather.find_or_create_station(@station_attrs)
      assert station.station_code == "KDFW"
      assert station.station_type == "asos"
    end

    test "returns existing station if already created" do
      {:ok, first} = Weather.find_or_create_station(@station_attrs)
      {:ok, second} = Weather.find_or_create_station(@station_attrs)
      assert first.id == second.id
    end

    test "returns error for invalid attributes" do
      assert {:error, _changeset} = Weather.find_or_create_station(%{station_code: "X"})
    end
  end

  describe "upsert_surface_observation/2" do
    test "inserts a new observation" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      obs_attrs = %{
        observed_at: ~U[2026-03-28 18:00:00Z],
        temp_f: 75.0,
        dewpoint_f: 55.0,
        sky_condition: "SCT"
      }

      assert {:ok, obs} = Weather.upsert_surface_observation(station, obs_attrs)
      assert obs.temp_f == 75.0
      assert obs.station_id == station.id
    end

    test "updates existing observation on conflict" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      obs_attrs = %{
        observed_at: ~U[2026-03-28 18:00:00Z],
        temp_f: 75.0,
        sky_condition: "SCT"
      }

      {:ok, first} = Weather.upsert_surface_observation(station, obs_attrs)
      {:ok, second} = Weather.upsert_surface_observation(station, %{obs_attrs | temp_f: 78.0})

      assert first.id == second.id
      assert second.temp_f == 78.0
    end
  end

  describe "upsert_sounding/2" do
    @sample_profile [
      %{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0, "drct" => 180, "sknt" => 10},
      %{"pres" => 925.0, "hght" => 800, "tmpc" => 20.0, "dwpc" => 12.0, "drct" => 200, "sknt" => 15},
      %{"pres" => 850.0, "hght" => 1500, "tmpc" => 15.0, "dwpc" => 8.0, "drct" => 220, "sknt" => 20}
    ]

    test "inserts a new sounding" do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      sounding_attrs = %{
        observed_at: ~U[2026-03-28 12:00:00Z],
        profile: @sample_profile,
        level_count: 3,
        surface_temp_c: 25.0
      }

      assert {:ok, sounding} = Weather.upsert_sounding(station, sounding_attrs)
      assert sounding.level_count == 3
      assert sounding.station_id == station.id
    end

    test "updates existing sounding on conflict" do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      sounding_attrs = %{
        observed_at: ~U[2026-03-28 12:00:00Z],
        profile: @sample_profile,
        level_count: 3,
        surface_temp_c: 25.0
      }

      {:ok, first} = Weather.upsert_sounding(station, sounding_attrs)
      {:ok, second} = Weather.upsert_sounding(station, %{sounding_attrs | surface_temp_c: 26.0})

      assert first.id == second.id
      assert second.surface_temp_c == 26.0
    end
  end

  describe "weather_for_contact/2" do
    test "returns nearby surface observations within time window with station preloaded" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      {:ok, _obs} =
        Weather.upsert_surface_observation(station, %{
          observed_at: ~U[2026-03-28 18:00:00Z],
          temp_f: 75.0
        })

      # QSO near the station in space and time
      result =
        Weather.weather_for_contact(
          %{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 18:30:00Z]},
          radius_km: 100,
          time_window_hours: 3
        )

      assert length(result.surface_observations) == 1
      obs = hd(result.surface_observations)
      assert obs.temp_f == 75.0
      assert obs.station.station_code == "KDFW"
    end

    test "returns nearby soundings within time window with station preloaded" do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      {:ok, _sounding} =
        Weather.upsert_sounding(station, %{
          observed_at: ~U[2026-03-28 12:00:00Z],
          profile: [%{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0, "drct" => 180, "sknt" => 10}],
          level_count: 1,
          surface_temp_c: 25.0
        })

      result =
        Weather.weather_for_contact(
          %{lat: 32.85, lon: -97.28, timestamp: ~U[2026-03-28 14:00:00Z]},
          radius_km: 100,
          time_window_hours: 6
        )

      assert length(result.soundings) == 1
      assert hd(result.soundings).station.station_code == "FWD"
    end

    test "excludes observations outside radius" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      {:ok, _obs} =
        Weather.upsert_surface_observation(station, %{
          observed_at: ~U[2026-03-28 18:00:00Z],
          temp_f: 75.0
        })

      # QSO far away
      result =
        Weather.weather_for_contact(
          %{lat: 40.0, lon: -80.0, timestamp: ~U[2026-03-28 18:00:00Z]},
          radius_km: 100,
          time_window_hours: 3
        )

      assert result.surface_observations == []
    end

    test "excludes observations outside time window" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      {:ok, _obs} =
        Weather.upsert_surface_observation(station, %{
          observed_at: ~U[2026-03-28 06:00:00Z],
          temp_f: 75.0
        })

      # QSO much later
      result =
        Weather.weather_for_contact(
          %{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 22:00:00Z]},
          radius_km: 100,
          time_window_hours: 3
        )

      assert result.surface_observations == []
    end
  end

  describe "soundings_with_widening_radius/1" do
    test "returns soundings at the first radius that finds any" do
      # Station ~220 km east of the query point: outside the 150 km ring
      # but inside the 300 km ring.
      {:ok, far_station} =
        Weather.find_or_create_station(%{
          station_code: "FAR",
          station_type: "sounding",
          wmo_number: 72_001,
          name: "Far Site",
          lat: 32.9,
          lon: -94.6
        })

      Weather.upsert_sounding(far_station, %{
        observed_at: ~U[2026-03-28 12:00:00Z],
        profile: [%{"pres" => 1013, "hght" => 100, "tmpc" => 20, "dwpc" => 10}],
        level_count: 1
      })

      result =
        Weather.soundings_with_widening_radius(%{
          lat: 32.9,
          lon: -97.0,
          timestamp: ~U[2026-03-28 14:00:00Z]
        })

      assert result.soundings != []
      # 130 km is outside 150 but inside 300
      assert result.radius_km == 300
      refute result.exhausted
    end

    test "reports exhausted when no soundings exist within the widest radius" do
      result =
        Weather.soundings_with_widening_radius(%{
          lat: 32.9,
          lon: -97.0,
          timestamp: ~U[2026-03-28 14:00:00Z]
        })

      assert result.soundings == []
      assert result.exhausted
      assert result.radius_km == 1000
    end
  end

  describe "has_surface_observations?/3" do
    test "returns true when observations exist in the time window" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      Weather.upsert_surface_observation(station, %{
        observed_at: ~U[2026-03-28 18:00:00Z],
        temp_f: 75.0
      })

      assert Weather.has_surface_observations?(
               station.id,
               ~U[2026-03-28 16:00:00Z],
               ~U[2026-03-28 20:00:00Z]
             )
    end

    test "returns false when no observations exist in the time window" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      refute Weather.has_surface_observations?(
               station.id,
               ~U[2026-03-28 16:00:00Z],
               ~U[2026-03-28 20:00:00Z]
             )
    end

    test "returns false when observations are outside the time window" do
      {:ok, station} = Weather.find_or_create_station(@station_attrs)

      Weather.upsert_surface_observation(station, %{
        observed_at: ~U[2026-03-28 06:00:00Z],
        temp_f: 60.0
      })

      refute Weather.has_surface_observations?(
               station.id,
               ~U[2026-03-28 16:00:00Z],
               ~U[2026-03-28 20:00:00Z]
             )
    end
  end

  describe "has_sounding?/2" do
    test "returns true when a sounding exists at the given time" do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      Weather.upsert_sounding(station, %{
        observed_at: ~U[2026-03-28 12:00:00Z],
        profile: [%{"pres" => 1013.0, "tmpc" => 25.0}],
        level_count: 1
      })

      assert Weather.has_sounding?(station.id, ~U[2026-03-28 12:00:00Z])
    end

    test "returns false when no sounding exists at the given time" do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      refute Weather.has_sounding?(station.id, ~U[2026-03-28 12:00:00Z])
    end
  end

  describe "nearest_sounding_to/3" do
    @profile [
      %{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0},
      %{"pres" => 925.0, "hght" => 800, "tmpc" => 20.0, "dwpc" => 12.0}
    ]

    setup do
      {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)

      {:ok, sounding} =
        Weather.upsert_sounding(station, %{
          observed_at: ~U[2026-03-28 12:00:00Z],
          profile: @profile,
          level_count: 2,
          ducting_detected: true,
          min_refractivity_gradient: -250.0
        })

      %{station: station, sounding: sounding}
    end

    test "returns the closest sounding within spatial + temporal window", %{sounding: sounding} do
      # Roughly 30 km north of Fort Worth sounding station, 1h after the obs.
      assert {:ok, found} =
               Weather.nearest_sounding_to(33.10, -97.30, ~U[2026-03-28 13:00:00Z])

      assert found.id == sounding.id
      assert found.ducting_detected == true
    end

    test "returns {:error, :not_found} when no sounding is in range" do
      # Far outside the default 300 km / ±3h window.
      assert {:error, :not_found} =
               Weather.nearest_sounding_to(45.0, -80.0, ~U[2026-03-28 12:00:00Z])
    end

    test "returns {:error, :not_found} when the sounding is outside the time window" do
      # Same location as the sounding, 8 hours later — outside ±3h.
      assert {:error, :not_found} =
               Weather.nearest_sounding_to(32.83, -97.30, ~U[2026-03-28 20:00:00Z])
    end
  end

  describe "existing_solar_dates/0" do
    test "returns set of dates already in the database" do
      Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2})
      Weather.upsert_solar_index(%{date: ~D[2024-06-16], sfi: 145.0})

      dates = Weather.existing_solar_dates()
      assert MapSet.member?(dates, ~D[2024-06-15])
      assert MapSet.member?(dates, ~D[2024-06-16])
      refute MapSet.member?(dates, ~D[2024-06-17])
    end

    test "returns empty set when no solar indices exist" do
      assert Weather.existing_solar_dates() == MapSet.new()
    end
  end

  describe "nearby_stations/4" do
    test "returns stations within radius" do
      {:ok, _near} =
        Weather.find_or_create_station(%{
          station_code: "KDFW",
          station_type: "asos",
          name: "DFW Airport",
          lat: 32.90,
          lon: -97.04
        })

      {:ok, _far} =
        Weather.find_or_create_station(%{
          station_code: "KORD",
          station_type: "asos",
          name: "Chicago O'Hare",
          lat: 41.98,
          lon: -87.90
        })

      stations = Weather.nearby_stations(32.85, -97.05, "asos", 50)
      codes = Enum.map(stations, & &1.station_code)
      assert "KDFW" in codes
      refute "KORD" in codes
    end

    test "filters by station type" do
      {:ok, _asos} =
        Weather.find_or_create_station(%{
          station_code: "KDFW",
          station_type: "asos",
          name: "DFW Airport",
          lat: 32.90,
          lon: -97.04
        })

      {:ok, _sounding} =
        Weather.find_or_create_station(%{
          station_code: "FWD",
          station_type: "sounding",
          name: "Fort Worth",
          lat: 32.83,
          lon: -97.30
        })

      stations = Weather.nearby_stations(32.85, -97.10, "sounding", 100)
      codes = Enum.map(stations, & &1.station_code)
      assert "FWD" in codes
      refute "KDFW" in codes
    end

    test "returns empty list when no stations in radius" do
      {:ok, _} =
        Weather.find_or_create_station(%{
          station_code: "KDFW",
          station_type: "asos",
          name: "DFW Airport",
          lat: 32.90,
          lon: -97.04
        })

      assert Weather.nearby_stations(45.0, -80.0, "asos", 50) == []
    end
  end

  describe "sounding_times_around/1" do
    test "returns bracketing 00Z and 12Z for afternoon UTC" do
      dt = ~U[2026-03-28 18:00:00Z]
      times = Weather.sounding_times_around(dt)
      assert times == [~U[2026-03-28 00:00:00Z], ~U[2026-03-28 12:00:00Z]]
    end

    test "returns bracketing times for morning UTC" do
      dt = ~U[2026-03-28 06:00:00Z]
      times = Weather.sounding_times_around(dt)
      assert times == [~U[2026-03-27 12:00:00Z], ~U[2026-03-28 00:00:00Z]]
    end

    test "returns single time at exactly 00Z" do
      dt = ~U[2026-03-28 00:00:00Z]
      times = Weather.sounding_times_around(dt)
      assert times == [~U[2026-03-27 12:00:00Z], ~U[2026-03-28 00:00:00Z]]
    end

    test "returns single time at exactly 12Z" do
      dt = ~U[2026-03-28 12:00:00Z]
      times = Weather.sounding_times_around(dt)
      assert times == [~U[2026-03-28 00:00:00Z], ~U[2026-03-28 12:00:00Z]]
    end
  end

  describe "get_solar_index/1" do
    test "returns the solar index record for a given date" do
      {:ok, record} = Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2, sunspot_number: 120})

      found = Weather.get_solar_index(~D[2024-06-15])
      assert found.id == record.id
      assert found.sfi == 150.2
    end

    test "returns nil when no record exists" do
      assert Weather.get_solar_index(~D[2024-01-01]) == nil
    end
  end

  describe "upsert_solar_index/1" do
    test "inserts a new solar index record" do
      attrs = %{
        date: ~D[2024-06-15],
        sfi: 150.2,
        sfi_adjusted: 148.5,
        sunspot_number: 120,
        ap_index: 8,
        kp_values: [2.0, 1.667, 2.333, 3.0, 2.667, 1.333, 1.0, 1.667]
      }

      assert {:ok, record} = Weather.upsert_solar_index(attrs)
      assert record.date == ~D[2024-06-15]
      assert record.sfi == 150.2
      assert record.sunspot_number == 120
      assert record.ap_index == 8
      assert length(record.kp_values) == 8
    end

    test "updates existing record on date conflict" do
      attrs = %{
        date: ~D[2024-06-15],
        sfi: 150.2,
        sfi_adjusted: 148.5,
        sunspot_number: 120,
        ap_index: 8,
        kp_values: [2.0, 1.667, 2.333, 3.0, 2.667, 1.333, 1.0, 1.667]
      }

      {:ok, first} = Weather.upsert_solar_index(attrs)
      {:ok, second} = Weather.upsert_solar_index(%{attrs | sfi: 155.0})

      assert first.id == second.id
      assert second.sfi == 155.0
    end

    test "inserts record with nil optional fields" do
      attrs = %{date: ~D[1932-01-01], sfi: nil, sfi_adjusted: nil, sunspot_number: 22, ap_index: 15}

      assert {:ok, record} = Weather.upsert_solar_index(attrs)
      assert record.date == ~D[1932-01-01]
      assert is_nil(record.sfi)
      assert record.sunspot_number == 22
    end
  end

  @hrrr_profile_attrs %{
    valid_time: ~U[2026-03-28 18:00:00Z],
    lat: 32.90,
    lon: -97.04,
    run_time: ~U[2026-03-28 18:00:00Z],
    profile: [
      %{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0},
      %{"pres" => 975.0, "hght" => 350, "tmpc" => 23.0, "dwpc" => 16.0}
    ],
    hpbl_m: 1500.0,
    pwat_mm: 25.0,
    surface_temp_c: 25.0,
    surface_dewpoint_c: 18.0,
    surface_pressure_mb: 1013.0,
    surface_refractivity: 320.5,
    min_refractivity_gradient: -45.0,
    ducting_detected: false
  }

  describe "upsert_hrrr_profile/1" do
    test "inserts a new HRRR profile" do
      assert {:ok, profile} = Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
      assert profile.lat == 32.90
      assert profile.hpbl_m == 1500.0
    end

    test "preserves existing profile on conflict" do
      {:ok, first} = Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
      Weather.upsert_hrrr_profile(%{@hrrr_profile_attrs | hpbl_m: 2000.0})

      reloaded = Repo.get!(HrrrProfile, first.id)
      assert reloaded.hpbl_m == 1500.0
      assert Repo.aggregate(HrrrProfile, :count) == 1
    end
  end

  describe "purge_grid_point_profiles/0" do
    test "deletes all grid-point rows across partitions while preserving QSO-linked rows" do
      # QSO-linked row (is_grid_point defaults to false)
      {:ok, qso_linked} = Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      # Grid-point row in a historical partition
      Repo.query!(
        """
        INSERT INTO hrrr_profiles
          (id, valid_time, lat, lon, is_grid_point, inserted_at, updated_at)
        VALUES ($1, $2, $3, $4, true, $5, $5)
        """,
        [
          Ecto.UUID.bingenerate(),
          ~N[2023-07-15 12:00:00],
          32.125,
          -97.125,
          NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
        ]
      )

      # Grid-point row in the current partition
      Repo.query!(
        """
        INSERT INTO hrrr_profiles
          (id, valid_time, lat, lon, is_grid_point, inserted_at, updated_at)
        VALUES ($1, $2, $3, $4, true, $5, $5)
        """,
        [
          Ecto.UUID.bingenerate(),
          NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
          33.125,
          -96.125,
          NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
        ]
      )

      assert Repo.aggregate(HrrrProfile, :count) == 3
      assert Weather.purge_grid_point_profiles() == 2

      remaining = Repo.all(HrrrProfile)
      assert length(remaining) == 1
      assert hd(remaining).id == qso_linked.id
    end

    test "returns 0 when there are no grid-point rows" do
      {:ok, _} = Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
      assert Weather.purge_grid_point_profiles() == 0
      assert Repo.aggregate(HrrrProfile, :count) == 1
    end
  end

  describe "has_hrrr_profile?/3" do
    test "returns true when a profile exists at the given location and time" do
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      assert Weather.has_hrrr_profile?(32.90, -97.04, ~U[2026-03-28 18:00:00Z])
    end

    test "returns false when no profile exists" do
      refute Weather.has_hrrr_profile?(32.90, -97.04, ~U[2026-03-28 18:00:00Z])
    end

    test "returns false for different location" do
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      refute Weather.has_hrrr_profile?(40.0, -80.0, ~U[2026-03-28 18:00:00Z])
    end
  end

  describe "hrrr_for_contact/1" do
    test "returns nearest HRRR profile for a QSO" do
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      contact = %{
        pos1: %{"lat" => 32.91, "lon" => -97.05},
        qso_timestamp: ~U[2026-03-28 18:30:00Z]
      }

      profile = Weather.hrrr_for_contact(contact)
      assert profile
      assert profile.lat == 32.90
    end

    test "returns nil when no HRRR profiles exist nearby" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      assert Weather.hrrr_for_contact(contact) == nil
    end

    test "returns nil when QSO has no pos1" do
      assert Weather.hrrr_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
    end
  end

  describe "round_to_hrrr_grid/2" do
    test "rounds coordinates to 2 decimal places" do
      assert Weather.round_to_hrrr_grid(32.9047, -97.0382) == {32.90, -97.04}
    end

    test "handles negative values correctly" do
      assert Weather.round_to_hrrr_grid(-33.456, -97.999) == {-33.46, -98.0}
    end
  end

  describe "round_to_iemre_grid/2" do
    test "rounds to nearest 0.125 degree" do
      assert Weather.round_to_iemre_grid(32.9047, -97.0382) == {32.875, -97.0}
    end

    test "handles exact grid points" do
      assert Weather.round_to_iemre_grid(33.0, -97.125) == {33.0, -97.125}
    end

    test "handles negative values correctly" do
      assert Weather.round_to_iemre_grid(-33.44, -97.99) == {-33.5, -98.0}
    end
  end

  @iemre_attrs %{
    lat: 32.875,
    lon: -97.0,
    date: ~D[2026-03-28],
    hourly: [%{"hour" => 0, "p01m_mm" => 0.0}]
  }

  describe "upsert_iemre_observation/1" do
    test "inserts a new IEMRE observation" do
      assert {:ok, obs} = Weather.upsert_iemre_observation(@iemre_attrs)
      assert obs.lat == 32.875
      assert obs.date == ~D[2026-03-28]
    end

    test "preserves existing observation on conflict" do
      {:ok, first} = Weather.upsert_iemre_observation(@iemre_attrs)

      Weather.upsert_iemre_observation(%{
        @iemre_attrs
        | hourly: [%{"hour" => 0, "p01m_mm" => 1.5}]
      })

      reloaded = Repo.get!(IemreObservation, first.id)
      assert hd(reloaded.hourly)["p01m_mm"] == 0.0
      assert Repo.aggregate(IemreObservation, :count) == 1
    end
  end

  describe "has_iemre_observation?/3" do
    test "returns true when observation exists" do
      Weather.upsert_iemre_observation(@iemre_attrs)
      assert Weather.has_iemre_observation?(32.875, -97.0, ~D[2026-03-28])
    end

    test "returns false when no observation exists" do
      refute Weather.has_iemre_observation?(32.875, -97.0, ~D[2026-03-28])
    end

    test "returns false for different location" do
      Weather.upsert_iemre_observation(@iemre_attrs)
      refute Weather.has_iemre_observation?(40.0, -80.0, ~D[2026-03-28])
    end
  end

  describe "hrrr_profiles_for_path/1" do
    test "returns HRRR profiles for all path points" do
      # Profile at pos1
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      # Profile near pos2
      Weather.upsert_hrrr_profile(%{
        @hrrr_profile_attrs
        | lat: 30.30,
          lon: -97.70
      })

      contact = %{
        pos1: %{"lat" => 32.91, "lon" => -97.05},
        pos2: %{"lat" => 30.31, "lon" => -97.71},
        qso_timestamp: ~U[2026-03-28 18:30:00Z]
      }

      profiles = Weather.hrrr_profiles_for_path(contact)
      assert length(profiles) >= 2
    end

    test "returns empty list when no HRRR profiles exist nearby" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        pos2: %{"lat" => 41.0, "lon" => -81.0},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      assert Weather.hrrr_profiles_for_path(contact) == []
    end

    test "returns profiles for pos1 only when pos2 is nil" do
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      contact = %{
        pos1: %{"lat" => 32.91, "lon" => -97.05},
        pos2: nil,
        qso_timestamp: ~U[2026-03-28 18:30:00Z]
      }

      profiles = Weather.hrrr_profiles_for_path(contact)
      assert length(profiles) == 1
    end
  end

  describe "iemre_for_path/1" do
    test "returns IEMRE observations for all path points" do
      Weather.upsert_iemre_observation(@iemre_attrs)

      # Observation near pos2
      Weather.upsert_iemre_observation(%{
        lat: 30.25,
        lon: -97.75,
        date: ~D[2026-03-28],
        hourly: [%{"hour" => 0, "p01m_mm" => 1.0}]
      })

      contact = %{
        pos1: %{"lat" => 32.9, "lon" => -97.0},
        pos2: %{"lat" => 30.3, "lon" => -97.7},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      observations = Weather.iemre_for_path(contact)
      assert length(observations) >= 2
    end

    test "returns empty list when no observations exist" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        pos2: %{"lat" => 41.0, "lon" => -81.0},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      assert Weather.iemre_for_path(contact) == []
    end

    test "returns observation for pos1 only when pos2 is nil" do
      Weather.upsert_iemre_observation(@iemre_attrs)

      contact = %{
        pos1: %{"lat" => 32.9, "lon" => -97.0},
        pos2: nil,
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      observations = Weather.iemre_for_path(contact)
      assert length(observations) == 1
    end
  end

  describe "find_nearest_hrrr/3" do
    test "returns nearest HRRR profile for given lat, lon, and timestamp" do
      Weather.upsert_hrrr_profile(@hrrr_profile_attrs)

      profile = Weather.find_nearest_hrrr(32.91, -97.05, ~U[2026-03-28 18:30:00Z])
      assert profile
      assert profile.lat == 32.90
    end

    test "returns nil when no HRRR profiles exist nearby" do
      assert Weather.find_nearest_hrrr(40.0, -80.0, ~U[2026-03-28 18:00:00Z]) == nil
    end
  end

  describe "find_nearest_native_profile/3" do
    @native_attrs %{
      valid_time: ~U[2026-03-28 18:00:00Z],
      run_time: ~U[2026-03-28 18:00:00Z],
      lat: 32.90,
      lon: -97.04,
      level_count: 2,
      heights_m: [10.0, 500.0],
      temp_k: [295.0, 285.0],
      spfh: [0.010, 0.004],
      pressure_pa: [101_000.0, 95_000.0],
      u_wind_ms: [2.0, 3.0],
      v_wind_ms: [1.0, 1.5],
      tke_m2s2: [0.5, 0.1]
    }

    test "returns nearest native profile within bounding box + time window" do
      {:ok, _} =
        %HrrrNativeProfile{}
        |> HrrrNativeProfile.changeset(@native_attrs)
        |> Microwaveprop.Repo.insert()

      profile = Weather.find_nearest_native_profile(32.91, -97.05, ~U[2026-03-28 18:20:00Z])
      assert profile
      assert profile.lat == 32.90
      assert profile.level_count == 2
    end

    test "returns nil when no native profile exists nearby" do
      assert Weather.find_nearest_native_profile(40.0, -80.0, ~U[2026-03-28 18:00:00Z]) == nil
    end

    test "returns nil when the nearest profile is outside the time window" do
      {:ok, _} =
        %HrrrNativeProfile{}
        |> HrrrNativeProfile.changeset(@native_attrs)
        |> Microwaveprop.Repo.insert()

      # 3+ hours away from the stored 18:00Z profile.
      assert Weather.find_nearest_native_profile(32.91, -97.05, ~U[2026-03-28 22:00:00Z]) ==
               nil
    end
  end

  describe "iemre_for_contact/1" do
    test "returns IEMRE observation matching QSO pos1 and date" do
      Weather.upsert_iemre_observation(@iemre_attrs)

      contact = %{
        pos1: %{"lat" => 32.9, "lon" => -97.0},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      obs = Weather.iemre_for_contact(contact)
      assert obs
      assert obs.lat == 32.875
    end

    test "returns nil when no IEMRE observation exists" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        qso_timestamp: ~U[2026-03-28 18:00:00Z]
      }

      assert Weather.iemre_for_contact(contact) == nil
    end

    test "returns nil when QSO has no pos1" do
      assert Weather.iemre_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
    end
  end
end
