defmodule Microwaveprop.Propagation.CalibrationTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Propagation.Calibration

  describe "spearman/2" do
    test "perfect monotonic correlation is 1.0" do
      assert_in_delta Calibration.spearman([1, 2, 3, 4, 5], [10, 20, 30, 40, 50]), 1.0, 1.0e-9
    end

    test "perfect inverse correlation is -1.0" do
      assert_in_delta Calibration.spearman([1, 2, 3, 4, 5], [50, 40, 30, 20, 10]), -1.0, 1.0e-9
    end

    test "uncorrelated input lands near zero" do
      xs = Enum.to_list(1..100)
      # constant ys → undefined ranking; we expect 0 by convention.
      ys = List.duplicate(42, 100)
      result = Calibration.spearman(xs, ys)
      assert result == nil or abs(result) < 0.05
    end

    test "returns nil for fewer than 3 pairs" do
      assert Calibration.spearman([1], [1]) == nil
      assert Calibration.spearman([1, 2], [3, 4]) == nil
    end
  end

  describe "rmse/2" do
    test "zero for identical lists" do
      assert Calibration.rmse([1, 2, 3], [1, 2, 3]) == 0.0
    end

    test "matches the textbook formula" do
      # mean of [1, 4, 9] = 14/3 ; sqrt = 2.16
      assert_in_delta Calibration.rmse([1, 2, 3], [2, 4, 6]), :math.sqrt(14 / 3), 1.0e-9
    end

    test "returns nil for empty input" do
      assert Calibration.rmse([], []) == nil
    end
  end

  describe "bucket_by_score/2" do
    test "splits samples into 5 fixed score buckets and reports n + median distance" do
      samples = [
        %{algorithm_score: 5, distance_km: 50},
        %{algorithm_score: 15, distance_km: 60},
        %{algorithm_score: 25, distance_km: 100},
        %{algorithm_score: 45, distance_km: 200},
        %{algorithm_score: 65, distance_km: 300},
        %{algorithm_score: 85, distance_km: 500}
      ]

      buckets = Calibration.bucket_by_score(samples, :algorithm_score)
      assert length(buckets) == 5

      # First bucket is 0-20 with two contacts
      assert hd(buckets) == %{
               bucket: "0-20",
               n: 2,
               median_distance_km: 55.0
             }

      # Last bucket is 80-100 with one contact
      last = List.last(buckets)
      assert last.bucket == "80-100"
      assert last.n == 1
      assert last.median_distance_km == 500.0
    end

    test "returns 5 buckets even when some are empty" do
      buckets = Calibration.bucket_by_score([%{algorithm_score: 50, distance_km: 100}], :algorithm_score)
      assert length(buckets) == 5
      empty = Enum.filter(buckets, &(&1.n == 0))
      assert length(empty) == 4
    end
  end

  describe "band_summary/1" do
    test "computes n, mean scores, RMSE, and Spearman correlations" do
      samples =
        for i <- 1..10 do
          %{algorithm_score: i * 10, ml_score: i * 10 + 2, distance_km: i * 50}
        end

      summary = Calibration.band_summary(samples)
      assert summary.n == 10
      assert summary.mean_algorithm_score == 55.0
      assert summary.mean_ml_score == 57.0
      assert_in_delta summary.alg_ml_rmse, 2.0, 1.0e-9
      assert_in_delta summary.alg_distance_spearman, 1.0, 1.0e-9
      assert_in_delta summary.ml_distance_spearman, 1.0, 1.0e-9
    end

    test "returns a zero-row summary for empty input" do
      summary = Calibration.band_summary([])
      assert summary.n == 0
      assert summary.alg_ml_rmse == nil
    end
  end

  describe "disagreements/2" do
    test "returns top N samples by absolute alg-ml score difference, descending" do
      samples = [
        %{id: 1, algorithm_score: 50, ml_score: 51},
        %{id: 2, algorithm_score: 50, ml_score: 80},
        %{id: 3, algorithm_score: 50, ml_score: 30},
        %{id: 4, algorithm_score: 50, ml_score: 49},
        %{id: 5, algorithm_score: 50, ml_score: 0}
      ]

      top = Calibration.disagreements(samples, 3)
      assert Enum.map(top, & &1.id) == [5, 2, 3]
    end
  end

  describe "detect_drift/3" do
    test "flags when current alg_ml_rmse exceeds the drift threshold" do
      assert {:drift, msg} = Calibration.detect_drift(:ml_drift, 12.0, threshold: 8.0)
      assert msg =~ "ML model has drifted"
    end

    test "no drift when within threshold" do
      assert :ok = Calibration.detect_drift(:ml_drift, 4.0, threshold: 8.0)
    end

    test "flags algorithm drift when current correlation falls below history median by margin" do
      history_corrs = [0.42, 0.45, 0.43, 0.44]

      assert {:drift, msg} =
               Calibration.detect_drift(:algorithm_drift, 0.25, history: history_corrs, margin: 0.10)

      assert msg =~ "Algorithm correlation dropped"
    end

    test "no algorithm drift if history empty" do
      assert :ok = Calibration.detect_drift(:algorithm_drift, 0.10, history: [], margin: 0.10)
    end
  end
end
