defmodule Microwaveprop.WeatherExtraTest do
  @moduledoc """
  Fills in coverage holes on lesser-used `Microwaveprop.Weather`
  context helpers: has_surface_observations?/3, has_sounding?/2,
  the two `station_ids_with_*` set-membership helpers,
  `existing_solar_dates/0`, `sounding_times_around/1`, and
  `hrrr_data_fully_present?/1`.

  Each is driven with a small, deterministic fixture — we care about
  the boolean / set return contracts rather than large real-world
  data shapes.
  """
  use Microwaveprop.DataCase, async: true
  use ExUnitProperties

  alias Microwaveprop.Repo
  alias Microwaveprop.Weather
  alias Microwaveprop.Weather.Sounding
  alias Microwaveprop.Weather.SurfaceObservation

  defp station!(attrs \\ %{}) do
    base = %{
      station_code: "TST#{System.unique_integer([:positive])}",
      station_type: "asos",
      name: "Test Station",
      lat: 33.0,
      lon: -97.0
    }

    {:ok, s} = Weather.find_or_create_station(Map.merge(base, attrs))
    s
  end

  describe "has_surface_observations?/3" do
    test "true when any observation falls inside the window" do
      station = station!()

      {:ok, _} =
        %SurfaceObservation{}
        |> SurfaceObservation.changeset(%{
          station_id: station.id,
          observed_at: ~U[2026-04-20 12:00:00Z],
          temp_f: 72.0
        })
        |> Repo.insert()

      assert Weather.has_surface_observations?(
               station.id,
               ~U[2026-04-20 00:00:00Z],
               ~U[2026-04-20 23:59:59Z]
             )
    end

    test "false when the window excludes every observation" do
      station = station!()

      {:ok, _} =
        %SurfaceObservation{}
        |> SurfaceObservation.changeset(%{
          station_id: station.id,
          observed_at: ~U[2026-04-20 12:00:00Z],
          temp_f: 72.0
        })
        |> Repo.insert()

      refute Weather.has_surface_observations?(
               station.id,
               ~U[2026-04-21 00:00:00Z],
               ~U[2026-04-21 23:59:59Z]
             )
    end

    test "false for a station with no observations" do
      station = station!()
      refute Weather.has_surface_observations?(station.id, ~U[2026-04-20 00:00:00Z], ~U[2026-04-20 23:59:59Z])
    end
  end

  describe "has_sounding?/2" do
    test "true when a sounding exists at the exact observed_at" do
      station = station!(%{station_type: "sounding"})

      {:ok, _} =
        %Sounding{}
        |> Sounding.changeset(%{
          station_id: station.id,
          observed_at: ~U[2026-04-20 12:00:00Z],
          profile: [],
          level_count: 0
        })
        |> Repo.insert()

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

    test "false when no sounding exists at the given time" do
      station = station!(%{station_type: "sounding"})
      refute Weather.has_sounding?(station.id, ~U[2026-04-20 12:00:00Z])
    end
  end

  describe "station_ids_with_* set-membership helpers" do
    test "station_ids_with_surface_observations returns only the stations that have rows in the window" do
      a = station!()
      b = station!()
      c = station!()

      {:ok, _} =
        %SurfaceObservation{}
        |> SurfaceObservation.changeset(%{
          station_id: a.id,
          observed_at: ~U[2026-04-20 12:00:00Z],
          temp_f: 60.0
        })
        |> Repo.insert()

      {:ok, _} =
        %SurfaceObservation{}
        |> SurfaceObservation.changeset(%{
          station_id: b.id,
          observed_at: ~U[2026-04-20 13:00:00Z],
          temp_f: 70.0
        })
        |> Repo.insert()

      present =
        Weather.station_ids_with_surface_observations(
          [a.id, b.id, c.id],
          ~U[2026-04-20 00:00:00Z],
          ~U[2026-04-20 23:59:59Z]
        )

      assert MapSet.size(present) == 2
      assert MapSet.member?(present, a.id)
      assert MapSet.member?(present, b.id)
      refute MapSet.member?(present, c.id)
    end

    test "station_ids_with_soundings returns {station_id, observed_at} tuples" do
      station = station!(%{station_type: "sounding"})

      {:ok, _} =
        %Sounding{}
        |> Sounding.changeset(%{
          station_id: station.id,
          observed_at: ~U[2026-04-20 12:00:00Z],
          profile: [],
          level_count: 0
        })
        |> Repo.insert()

      set =
        Weather.station_ids_with_soundings(
          [station.id],
          [~U[2026-04-20 12:00:00Z], ~U[2026-04-20 00:00:00Z]]
        )

      assert MapSet.member?(set, {station.id, ~U[2026-04-20 12:00:00Z]})
      refute MapSet.member?(set, {station.id, ~U[2026-04-20 00:00:00Z]})
    end
  end

  describe "sounding_times_around/1" do
    # Radiosondes fly twice a day (00Z, 12Z). The helper returns the
    # two nearest bracketing times for a given wall-clock UTC.
    test "early-morning times bracket prev-day 12Z and same-day 00Z" do
      times = Weather.sounding_times_around(~U[2026-04-20 06:00:00Z])

      assert length(times) >= 2
      assert ~U[2026-04-19 12:00:00Z] in times
      assert ~U[2026-04-20 00:00:00Z] in times
    end

    test "afternoon times bracket same-day 00Z/12Z" do
      times = Weather.sounding_times_around(~U[2026-04-20 18:00:00Z])
      assert ~U[2026-04-20 00:00:00Z] in times
      assert ~U[2026-04-20 12:00:00Z] in times
    end

    test "noon UTC lands on the 12Z bracket" do
      times = Weather.sounding_times_around(~U[2026-04-20 12:00:00Z])
      assert ~U[2026-04-20 12:00:00Z] in times
    end
  end

  describe "hrrr_data_fully_present?/1" do
    test "false when pos1 is missing" do
      refute Weather.hrrr_data_fully_present?(%{pos1: nil})
    end

    test "false when qso_timestamp is missing" do
      refute Weather.hrrr_data_fully_present?(%{qso_timestamp: nil, pos1: %{"lat" => 32.9, "lon" => -97.0}})
    end
  end

  describe "existing_solar_dates/0" do
    test "returns a MapSet of Dates for every row in solar_indices" do
      date = ~D[2026-04-20]

      Weather.upsert_solar_indices_batch([
        %{
          date: date,
          sfi: 120.0,
          sfi_adjusted: 118.0,
          sunspot_number: 55,
          ap_index: 5,
          kp_values: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
        }
      ])

      set = Weather.existing_solar_dates()
      assert MapSet.member?(set, date)
    end
  end

  describe "iemre_for_contact/1 / iemre_for_path/1 edge cases" do
    test "iemre_for_contact returns nil with empty DB and valid pos1" do
      contact = %{
        pos1: %{"lat" => 32.9, "lon" => -97.0},
        qso_timestamp: ~U[2026-04-20 18:00:00Z]
      }

      assert Weather.iemre_for_contact(contact) == nil
    end

    test "iemre_for_path returns [] when pos1 is nil" do
      assert Weather.iemre_for_path(%{pos1: nil}) == []
    end
  end

  describe "narr_for_contact/1 / narr_profiles_for_path/1 edge cases" do
    test "narr_for_contact returns nil when pos1 is nil" do
      assert Weather.narr_for_contact(%{pos1: nil}) == nil
    end

    test "narr_profiles_for_path returns [] when pos1 is nil" do
      assert Weather.narr_profiles_for_path(%{pos1: nil}) == []
    end

    test "narr_for_contact returns nil when coords present but no profile exists" do
      contact = %{
        pos1: %{"lat" => 32.9, "lon" => -97.0},
        qso_timestamp: ~U[2026-04-20 18:00:00Z]
      }

      assert Weather.narr_for_contact(contact) == nil
    end
  end

  describe "best_profile_for_contact/1 fallback behavior" do
    test "returns nil when neither HRRR nor NARR exists" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        qso_timestamp: ~U[2026-04-20 18:00:00Z]
      }

      assert Weather.best_profile_for_contact(contact) == nil
    end
  end

  describe "profiles_along_path/1 fallback" do
    test "returns [] when no HRRR or NARR is near the path" do
      contact = %{
        pos1: %{"lat" => 40.0, "lon" => -80.0},
        pos2: %{"lat" => 41.0, "lon" => -81.0},
        qso_timestamp: ~U[2026-04-20 18:00:00Z]
      }

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

  describe "nearest_native_duct_ghz/3 and nearest_native_duct_info/3" do
    test "returns nil / {:error, :not_found} with no native profile rows" do
      ts = ~U[2026-04-20 18:00:00Z]
      assert Weather.nearest_native_duct_ghz(32.9, -97.0, ts) == nil
      assert Weather.nearest_native_duct_info(32.9, -97.0, ts) == {:error, :not_found}
    end
  end

  describe "property: round_to_hrrr_grid/2" do
    property "rounded output stays within 0.005 of input for any CONUS lat/lon" do
      check all(
              lat <- float(min: 20.0, max: 55.0),
              lon <- float(min: -130.0, max: -60.0)
            ) do
        {rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)

        assert abs(rlat - lat) <= 0.005 + 1.0e-9
        assert abs(rlon - lon) <= 0.005 + 1.0e-9
        # Output is on a 0.01° grid
        assert_in_delta rlat * 100, round(rlat * 100), 1.0e-6
        assert_in_delta rlon * 100, round(rlon * 100), 1.0e-6
      end
    end
  end

  describe "property: round_to_iemre_grid/2" do
    property "rounded output lands on the 0.125° IEMRE grid within ±0.0625° of the input" do
      check all(
              lat <- float(min: -60.0, max: 60.0),
              lon <- float(min: -180.0, max: 180.0)
            ) do
        {rlat, rlon} = Weather.round_to_iemre_grid(lat, lon)

        # Must be on a 0.125° grid (8 per degree).
        assert_in_delta rlat * 8, round(rlat * 8), 1.0e-6
        assert_in_delta rlon * 8, round(rlon * 8), 1.0e-6

        # And never farther than half a step from the input.
        assert abs(rlat - lat) <= 0.0625 + 1.0e-9
        assert abs(rlon - lon) <= 0.0625 + 1.0e-9
      end
    end
  end

  describe "property: find_or_create_station/1 insert-then-query round-trip" do
    property "a newly created station is immediately returned by a duplicate call" do
      check all(
              suffix <- integer(1..1_000_000),
              lat <- float(min: 25.0, max: 49.0),
              lon <- float(min: -124.0, max: -66.0)
            ) do
        code = "ROUND#{suffix}"

        attrs = %{
          station_code: code,
          station_type: "asos",
          name: "Round-trip station",
          lat: lat,
          lon: lon
        }

        assert {:ok, first} = Weather.find_or_create_station(attrs)
        assert {:ok, second} = Weather.find_or_create_station(attrs)
        assert first.id == second.id
        assert second.station_code == code
      end
    end
  end
end
