defmodule Microwaveprop.Pskr.RecalibratorTest do
  use Microwaveprop.DataCase, async: true

  alias Microwaveprop.Pskr.CalibrationSample
  alias Microwaveprop.Pskr.FeatureBin
  alias Microwaveprop.Pskr.RecalibrationRun
  alias Microwaveprop.Pskr.Recalibrator
  alias Microwaveprop.Repo

  defp insert_sample!(attrs) do
    base = %{
      hour_utc: ~U[2026-05-04 18:00:00Z],
      band: "10000",
      midpoint_lat: 33.0,
      midpoint_lon: -97.0,
      spot_count: 5,
      pwat_mm: 30.0,
      hpbl_m: 500.0,
      min_refractivity_gradient: -100.0,
      surface_pressure_mb: 1013.0,
      kp_index: 2
    }

    {:ok, _} =
      %CalibrationSample{}
      |> CalibrationSample.changeset(Map.merge(base, attrs))
      |> Repo.insert()
  end

  # Insert N samples for a band, optionally varying one feature.
  # Uses a per-call unique offset so successive `insert_n!` invocations
  # don't collide on the (hour, band, midpoint_lat, midpoint_lon) key.
  defp insert_n!(n, band, attrs_fn) do
    offset = :erlang.unique_integer([:positive])

    Enum.each(1..n, fn i ->
      attrs = attrs_fn.(i)
      mp_lat = 33.0 + (offset * 1000 + i) * 0.0001
      mp_lon = -97.0 - (offset * 1000 + i) * 0.0001
      insert_sample!(Map.merge(attrs, %{band: band, midpoint_lat: mp_lat, midpoint_lon: mp_lon}))
    end)
  end

  describe "run/0" do
    test "skips with status `skipped_insufficient_data` when corpus is empty" do
      run = Recalibrator.run()

      assert run.status == "skipped_insufficient_data"
      assert run.sample_count == 0
      assert run.notes =~ "≥ 1000"
      assert Repo.aggregate(FeatureBin, :count) == 0
    end

    test "skips when corpus has < 1000 total samples" do
      insert_n!(500, "10000", fn _ -> %{} end)
      run = Recalibrator.run()

      assert run.status == "skipped_insufficient_data"
      assert run.sample_count == 500
      assert Repo.aggregate(FeatureBin, :count) == 0
    end

    test "completes and writes feature bins when corpus is dense enough" do
      # Spread samples across PWAT bins so several have non-zero counts.
      insert_n!(1200, "10000", fn i ->
        pwat = 10.0 + rem(i, 60)
        %{pwat_mm: pwat, spot_count: rem(i, 10) + 1}
      end)

      run = Recalibrator.run()
      assert run.status == "completed"
      assert run.sample_count == 1200
      assert run.band_count == 1
      assert run.avg_spots_per_sample > 0

      bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
      assert bins != []

      # Every bin row should reference the run, target this band, and
      # carry non-negative stats.
      Enum.each(bins, fn bin ->
        assert bin.run_id == run.id
        assert bin.band == "10000"
        assert bin.sample_count > 0
        assert bin.spot_count_total >= 0
      end)

      # PWAT should populate every defined bin label since we
      # spread values 10-69 across the range.
      pwat_bins =
        bins
        |> Enum.filter(&(&1.feature == "pwat_mm"))
        |> Enum.map(& &1.bin_label)
        |> Enum.sort()

      assert "<15" in pwat_bins
      assert "15-25" in pwat_bins
      assert "25-40" in pwat_bins
      assert "40-55" in pwat_bins
      assert ">55" in pwat_bins
    end

    test "skips bands below the per-band threshold even when total corpus passes" do
      # Band A gets enough samples; band B (50 samples) is too thin
      # to bin even though the total corpus passes the global
      # threshold.
      insert_n!(1100, "10000", fn _ -> %{spot_count: 3} end)
      insert_n!(50, "144", fn _ -> %{spot_count: 1} end)

      run = Recalibrator.run()
      assert run.status == "completed"
      assert run.sample_count == 1150
      # band_count counts distinct bands in the corpus, not just the
      # bands we ran analysis for, so 2 here is correct.
      assert run.band_count == 2

      bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
      bands_with_bins = bins |> Enum.map(& &1.band) |> Enum.uniq()

      assert "10000" in bands_with_bins
      refute "144" in bands_with_bins
    end

    test "spot_count_avg reflects the actual cell mean" do
      # Force every cell to have spot_count = 4 so we know the avg.
      insert_n!(1000, "10000", fn _ -> %{spot_count: 4} end)

      run = Recalibrator.run()
      pwat_bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id and fb.feature == "pwat_mm"))

      Enum.each(pwat_bins, fn bin ->
        if bin.sample_count > 0 do
          assert bin.spot_count_avg == 4.0
        end
      end)
    end

    test "ignores samples with nil feature value when binning that feature" do
      # Half have pwat_mm; half don't. The pwat bins should only
      # count the half that does.
      insert_n!(500, "10000", fn _ -> %{pwat_mm: 30.0, spot_count: 1} end)
      insert_n!(500, "10000", fn _ -> %{pwat_mm: nil, spot_count: 1} end)

      run = Recalibrator.run()
      assert run.status == "completed"

      pwat_total =
        FeatureBin
        |> where([fb], fb.run_id == ^run.id and fb.feature == "pwat_mm")
        |> Repo.all()
        |> Enum.map(& &1.sample_count)
        |> Enum.sum()

      assert pwat_total == 500
    end

    test "every run records a row in pskr_recalibration_runs regardless of status" do
      Recalibrator.run()
      Recalibrator.run()
      assert Repo.aggregate(RecalibrationRun, :count) == 2
    end
  end
end
