defmodule Microwaveprop.SpaceWeather.ObservationChangesetsTest do
  @moduledoc """
  Cast/validate coverage for the three space-weather observation
  schemas. These were historically at ~50 % line coverage because
  only the happy path of `swpc_client` exercised `changeset/2` — the
  required/optional splits and the unique constraints had no direct
  assertions.
  """
  use ExUnit.Case, async: true

  alias Microwaveprop.Ionosphere.Observation, as: IonoObs
  alias Microwaveprop.SpaceWeather.GeomagneticObservation
  alias Microwaveprop.SpaceWeather.SolarFluxObservation
  alias Microwaveprop.SpaceWeather.SolarXrayObservation
  alias Microwaveprop.Weather.HrrrClimatology

  describe "GeomagneticObservation.changeset/2" do
    test "valid with only valid_time" do
      cs = GeomagneticObservation.changeset(%GeomagneticObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
      assert cs.valid?
    end

    test "accepts kp_index and estimated_kp" do
      cs =
        GeomagneticObservation.changeset(%GeomagneticObservation{}, %{
          valid_time: ~U[2026-04-21 00:00:00Z],
          kp_index: 4,
          estimated_kp: 4.33
        })

      assert cs.valid?
      assert Ecto.Changeset.get_change(cs, :kp_index) == 4
      assert Ecto.Changeset.get_change(cs, :estimated_kp) == 4.33
    end

    test "invalid when valid_time is missing" do
      cs = GeomagneticObservation.changeset(%GeomagneticObservation{}, %{kp_index: 3})
      refute cs.valid?
      assert %{valid_time: ["can't be blank"]} = errors_on(cs)
    end
  end

  describe "SolarFluxObservation.changeset/2" do
    test "valid with required valid_time" do
      cs = SolarFluxObservation.changeset(%SolarFluxObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
      assert cs.valid?
    end

    test "accepts frequency, flux, schedule, and 90-day mean" do
      cs =
        SolarFluxObservation.changeset(%SolarFluxObservation{}, %{
          valid_time: ~U[2026-04-21 00:00:00Z],
          frequency_mhz: 2800,
          flux_sfu: 142.4,
          reporting_schedule: "daily",
          ninety_day_mean: 150.0
        })

      assert cs.valid?
    end

    test "invalid when valid_time is missing" do
      cs = SolarFluxObservation.changeset(%SolarFluxObservation{}, %{flux_sfu: 142.4})
      refute cs.valid?
      assert %{valid_time: ["can't be blank"]} = errors_on(cs)
    end
  end

  describe "SolarXrayObservation.changeset/2" do
    test "valid with required valid_time" do
      cs = SolarXrayObservation.changeset(%SolarXrayObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
      assert cs.valid?
    end

    test "casts satellite, energy_band, flux, and electron_contaminated" do
      cs =
        SolarXrayObservation.changeset(%SolarXrayObservation{}, %{
          valid_time: ~U[2026-04-21 00:00:00Z],
          satellite: 16,
          flux_wm2: 1.23e-6,
          energy_band: "0.1-0.8nm",
          electron_contaminated: true
        })

      assert cs.valid?
      assert Ecto.Changeset.get_change(cs, :electron_contaminated) == true
    end

    test "invalid when valid_time is missing" do
      cs = SolarXrayObservation.changeset(%SolarXrayObservation{}, %{flux_wm2: 1.0e-7})
      refute cs.valid?
      assert %{valid_time: ["can't be blank"]} = errors_on(cs)
    end
  end

  describe "Ionosphere.Observation.changeset/2" do
    test "valid with required station_code + valid_time" do
      cs =
        IonoObs.changeset(%IonoObs{}, %{
          station_code: "BC840",
          valid_time: ~U[2026-04-21 00:00:00Z]
        })

      assert cs.valid?
    end

    test "casts scaled characteristics" do
      cs =
        IonoObs.changeset(%IonoObs{}, %{
          station_code: "BC840",
          valid_time: ~U[2026-04-21 00:00:00Z],
          confidence_score: 75,
          fo_f2_mhz: 10.4,
          fo_e_mhz: 3.2,
          fo_es_mhz: 6.1,
          hm_f2_km: 275.0,
          mufd_mhz: 18.5
        })

      assert cs.valid?
      assert Ecto.Changeset.get_change(cs, :fo_f2_mhz) == 10.4
      assert Ecto.Changeset.get_change(cs, :confidence_score) == 75
    end

    test "invalid when either required field is missing" do
      cs1 = IonoObs.changeset(%IonoObs{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
      cs2 = IonoObs.changeset(%IonoObs{}, %{station_code: "BC840"})

      refute cs1.valid?
      refute cs2.valid?
      assert %{station_code: ["can't be blank"]} = errors_on(cs1)
      assert %{valid_time: ["can't be blank"]} = errors_on(cs2)
    end
  end

  describe "HrrrClimatology.changeset/2" do
    test "valid with the four required keys" do
      cs =
        HrrrClimatology.changeset(%HrrrClimatology{}, %{
          lat: 32.9,
          lon: -97.0,
          month: 7,
          hour: 15
        })

      assert cs.valid?
    end

    test "casts mean / stddev / sample_count" do
      cs =
        HrrrClimatology.changeset(%HrrrClimatology{}, %{
          lat: 32.9,
          lon: -97.0,
          month: 7,
          hour: 15,
          mean_surface_temp_c: 30.5,
          stddev_surface_temp_c: 4.2,
          sample_count: 365
        })

      assert cs.valid?
      assert Ecto.Changeset.get_change(cs, :mean_surface_temp_c) == 30.5
      assert Ecto.Changeset.get_change(cs, :sample_count) == 365
    end

    test "invalid when any of the four cell-identity keys is missing" do
      for missing <- [:lat, :lon, :month, :hour] do
        attrs = Map.delete(%{lat: 32.9, lon: -97.0, month: 7, hour: 15}, missing)

        cs = HrrrClimatology.changeset(%HrrrClimatology{}, attrs)
        refute cs.valid?, "expected #{missing} missing to invalidate"
        assert errors_on(cs)[missing] == ["can't be blank"]
      end
    end
  end

  # Local copy of the standard Phoenix.DataCase `errors_on/1` so this
  # file can stay on the lightweight `ExUnit.Case` (no DB sandbox
  # needed for changeset tests).
  defp errors_on(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
      Regex.replace(~r"%{(\w+)}", message, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end
