defmodule Microwaveprop.Weather.IemRateLimiterTest do
  use ExUnit.Case, async: false

  alias Microwaveprop.Weather.IemRateLimiter

  # Each test starts an isolated limiter under a unique name so the
  # app-level IemRateLimiter (started by the application supervisor
  # with interval_ms: 0) doesn't conflict.
  defp start_limiter(interval_ms) do
    name = :"iem_rate_limiter_test_#{System.unique_integer([:positive])}"
    start_supervised!({IemRateLimiter, interval_ms: interval_ms, name: name})
    name
  end

  describe "acquire/1" do
    test "first call returns immediately when the limiter is idle" do
      name = start_limiter(50)

      {elapsed_ms, :ok} = :timer.tc(fn -> IemRateLimiter.acquire(name) end, :millisecond)

      # Allow a generous upper bound for scheduling jitter; the point is that
      # no rate-limit sleep was applied on the first call.
      assert elapsed_ms < 20
    end

    test "serial calls are spaced at least interval_ms apart" do
      name = start_limiter(50)

      {elapsed_ms, _} =
        :timer.tc(
          fn ->
            IemRateLimiter.acquire(name)
            IemRateLimiter.acquire(name)
            IemRateLimiter.acquire(name)
          end,
          :millisecond
        )

      # Three acquires at 50ms gap → floor at 100ms (gap after call 1 and 2).
      assert elapsed_ms >= 100
    end

    test "concurrent callers each get a non-overlapping slot" do
      name = start_limiter(50)
      parent = self()

      for _ <- 1..4 do
        spawn(fn ->
          IemRateLimiter.acquire(name)
          send(parent, {:acquired, System.monotonic_time(:millisecond)})
        end)
      end

      timestamps =
        for _ <- 1..4 do
          assert_receive {:acquired, ts}, 500
          ts
        end

      sorted = Enum.sort(timestamps)
      gaps = sorted |> Enum.zip(tl(sorted)) |> Enum.map(fn {a, b} -> b - a end)
      min_gap = Enum.min(gaps)
      # Allow 5ms scheduler jitter on the lower bound.
      assert min_gap >= 45
    end

    test "interval_ms: 0 is a no-op" do
      name = start_limiter(0)

      {elapsed_ms, _} =
        :timer.tc(
          fn ->
            for _ <- 1..10, do: IemRateLimiter.acquire(name)
          end,
          :millisecond
        )

      assert elapsed_ms < 20
    end
  end

  describe "adaptive gap" do
    defp start_adaptive(base, max) do
      name = :"iem_rate_limiter_test_#{System.unique_integer([:positive])}"
      start_supervised!({IemRateLimiter, interval_ms: base, max_interval_ms: max, name: name})
      name
    end

    test "signal_429 widens the current gap" do
      name = start_adaptive(50, 500)
      IemRateLimiter.signal_429(name)
      IemRateLimiter.signal_429(name)
      gap = IemRateLimiter.current_interval_ms(name)
      assert gap > 50
      assert gap <= 500
    end

    test "signal_success decays the gap back toward base" do
      name = start_adaptive(50, 500)
      # Inflate the gap
      Enum.each(1..5, fn _ -> IemRateLimiter.signal_429(name) end)
      high_gap = IemRateLimiter.current_interval_ms(name)
      assert high_gap > 50

      # Successes drive it back down
      Enum.each(1..20, fn _ -> IemRateLimiter.signal_success(name) end)
      low_gap = IemRateLimiter.current_interval_ms(name)

      assert low_gap < high_gap
      # But never below the configured base.
      assert low_gap >= 50
    end

    test "max_interval_ms is a hard ceiling" do
      name = start_adaptive(100, 400)
      Enum.each(1..50, fn _ -> IemRateLimiter.signal_429(name) end)
      assert IemRateLimiter.current_interval_ms(name) == 400
    end
  end
end
