defmodule Microwaveprop.Propagation.FreshnessMonitorTest do
  @moduledoc """
  Characterization tests for `FreshnessMonitor`.

  The monitor is a simple GenServer that polls propagation score
  freshness on a 5-minute timer. On each tick it reads
  `Propagation.latest_valid_time/0` (which reads `ScoresFile`
  on disk) and enqueues a `PropagationGridWorker` job when either
  no scores exist yet or the newest score file is older than
  120 minutes.

  These tests drive the tick path synchronously by invoking
  `handle_info(:check, state)` from the test process inside
  `Oban.Testing.with_testing_mode(:manual, ...)` — that keeps
  `Oban.insert` from executing the job inline, letting us assert
  on enqueued jobs via `all_enqueued/1`. One smoke test exercises
  the supervised startup path with a fresh ScoresFile so no enqueue
  is attempted.

  The interval (`@check_interval = 5 min`) and staleness threshold
  (`@stale_threshold_minutes = 120`) are compile-time module
  attributes with no runtime override, so we can't shrink the
  interval — we exercise the tick directly instead.
  """

  use Microwaveprop.DataCase, async: false
  use Oban.Testing, repo: Microwaveprop.Repo

  alias Microwaveprop.Propagation.FreshnessMonitor
  alias Microwaveprop.Propagation.ScoresFile
  alias Microwaveprop.Workers.PropagationGridWorker

  @band_mhz 10_000
  @sample_scores [%{lat: 25.0, lon: -125.0, score: 50}]

  defp minutes_ago(n) do
    DateTime.utc_now()
    |> DateTime.add(-n, :minute)
    |> DateTime.truncate(:second)
  end

  describe "handle_info(:check, state) — characterization of tick behavior" do
    test "fresh grid: does not enqueue a PropagationGridWorker job" do
      ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
        assert [] = all_enqueued(worker: PropagationGridWorker)
      end)
    end

    test "stale grid (>120m old): enqueues a PropagationGridWorker job" do
      ScoresFile.write!(@band_mhz, minutes_ago(180), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})

        [job] = all_enqueued(worker: PropagationGridWorker)
        assert job.args == %{}
        assert job.queue == "propagation"
      end)
    end

    test "just past threshold (121m old): enqueues" do
      ScoresFile.write!(@band_mhz, minutes_ago(121), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
        assert [_job] = all_enqueued(worker: PropagationGridWorker)
      end)
    end

    test "exactly at threshold (120m old): does NOT enqueue (strict `>` comparison)" do
      ScoresFile.write!(@band_mhz, minutes_ago(120), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
        assert [] = all_enqueued(worker: PropagationGridWorker)
      end)
    end

    test "empty grid (no score files): enqueues a PropagationGridWorker job" do
      # DataCase.reset_score_files/0 wipes the scores dir, so nothing exists.
      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})

        [job] = all_enqueued(worker: PropagationGridWorker)
        assert job.args == %{}
      end)
    end

    test "returns unchanged state map from the tick" do
      Oban.Testing.with_testing_mode(:manual, fn ->
        assert {:noreply, %{foo: :bar}} = FreshnessMonitor.handle_info(:check, %{foo: :bar})
      end)
    end

    test "repeated ticks on a persistently stale grid are deduplicated into a single job" do
      # PropagationGridWorker declares `unique:` on the seed args (`%{}`),
      # so a stale-check tick that runs while an earlier seed is still
      # available/scheduled/executing/retryable is a no-op. Without this,
      # a long outage stacks one expensive HRRR f00-f18 chain per 5-min
      # tick (e.g. a 2-hour outage = 24 redundant seed jobs).
      ScoresFile.write!(@band_mhz, minutes_ago(180), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        FreshnessMonitor.handle_info(:check, %{})
        FreshnessMonitor.handle_info(:check, %{})
        FreshnessMonitor.handle_info(:check, %{})

        assert 1 == length(all_enqueued(worker: PropagationGridWorker))
      end)
    end

    test "tick does not synchronously deliver the next :check message (it is scheduled with a 5-minute delay)" do
      # Characterizes the `Process.send_after(self(), :check, @check_interval)`
      # call at the end of handle_info/2: the follow-up :check is
      # scheduled, not sent immediately, so the test mailbox stays
      # empty.
      ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)

      Oban.Testing.with_testing_mode(:manual, fn ->
        FreshnessMonitor.handle_info(:check, %{})
        refute_received :check
      end)
    end
  end

  describe "start_link/1 — supervision lifecycle" do
    test "does not start when start_freshness_monitor config is false (test default)" do
      # In test.exs the flag is false, so start_link returns :ignore
      # and no process is registered.
      assert :ignore = FreshnessMonitor.start_link([])
      assert Process.whereis(FreshnessMonitor) == nil
    end

    test "supervised start: boots cleanly with a fresh ScoresFile and no enqueue occurs" do
      ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)

      # Enable the start flag for this test only.
      original = Application.get_env(:microwaveprop, :start_freshness_monitor)
      Application.put_env(:microwaveprop, :start_freshness_monitor, true)

      on_exit(fn ->
        if is_nil(original) do
          Application.delete_env(:microwaveprop, :start_freshness_monitor)
        else
          Application.put_env(:microwaveprop, :start_freshness_monitor, original)
        end
      end)

      pid = start_supervised!(FreshnessMonitor)
      assert is_pid(pid)
      assert Process.alive?(pid)
      assert Process.whereis(FreshnessMonitor) == pid

      # Force the init-time :check to be processed by issuing a
      # synchronous call; GenServer.call flushes the mailbox up to
      # this point. FreshnessMonitor doesn't implement handle_call,
      # so use :sys.get_state instead — it waits for all pending
      # messages to be handled.
      assert %{} == :sys.get_state(pid)
    end
  end
end
