defmodule Microwaveprop.Propagation.NotifyListenerTest do
  # async: false — uses the shared :propagation_scores_dir + ScoreCache
  use ExUnit.Case, async: false

  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Propagation.Grid
  alias Microwaveprop.Propagation.NotifyListener
  alias Microwaveprop.Propagation.ProfilesFile
  alias Microwaveprop.Propagation.ScoreCache
  alias Microwaveprop.Propagation.ScoresFile
  alias Microwaveprop.Weather.ScalarFile

  setup do
    dir =
      Path.join(
        System.tmp_dir!(),
        "notify_listener_test_#{System.unique_integer([:positive])}"
      )

    File.mkdir_p!(dir)
    prev = Application.get_env(:microwaveprop, :propagation_scores_dir)
    Application.put_env(:microwaveprop, :propagation_scores_dir, dir)
    ScoreCache.clear()

    on_exit(fn ->
      File.rm_rf!(dir)
      ScoreCache.clear()

      if prev do
        Application.put_env(:microwaveprop, :propagation_scores_dir, prev)
      else
        Application.delete_env(:microwaveprop, :propagation_scores_dir)
      end
    end)

    :ok
  end

  defp sample_scores do
    %{lat_min: lat_min, lon_min: lon_min, lat_max: lat_max, lon_max: lon_max} = Grid.bounds()

    [
      %{lat: lat_min, lon: lon_min, score: 10},
      %{lat: lat_max, lon: lon_max, score: 90}
    ]
  end

  describe "handle_propagation_ready/1" do
    test "does NOT eagerly warm ScoreCache (lazy fill via on-disk reads)" do
      valid_time = ~U[2026-04-21 12:00:00Z]

      # Write all bands so the old code path would have warmed every one.
      for band <- BandConfig.all_bands() do
        ScoresFile.write!(band.freq_mhz, valid_time, sample_scores())
      end

      assert :ets.info(:propagation_score_cache, :size) == 0

      :ok = NotifyListener.handle_propagation_ready(valid_time)
      ScoreCache.sync()

      # Cache stays empty: NotifyListener no longer materialises the
      # 92k-cell grid for every band into ETS. LiveView callers fall
      # through to `read_from_disk_and_cache/3` on demand.
      assert :ets.info(:propagation_score_cache, :size) == 0
    end

    test "broadcasts propagation:updated to PubSub" do
      valid_time = ~U[2026-04-21 14:00:00Z]
      ScoresFile.write!(10_000, valid_time, sample_scores())

      Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")

      :ok = NotifyListener.handle_propagation_ready(valid_time)

      assert_receive {:propagation_updated, [^valid_time]}, 1_000
    end

    test "prunes ScoreCache entries outside the active forecast window" do
      stale = ~U[2026-04-12 00:00:00Z]
      ScoreCache.put(10_000, stale, [%{lat: 1.0, lon: 1.0, score: 1}])

      now_valid_time = DateTime.truncate(DateTime.utc_now(), :second)
      ScoresFile.write!(10_000, now_valid_time, sample_scores())

      :ok = NotifyListener.handle_propagation_ready(now_valid_time)

      assert ScoreCache.fetch(10_000, stale) == :miss
    end

    test "kicks off async ScalarFile materialization for the new valid_time" do
      vt = ~U[2026-04-21 16:00:00Z]
      ScoresFile.write!(10_000, vt, sample_scores())

      ProfilesFile.write!(vt, %{
        {32.125, -97.375} => %{
          surface_temp_c: 25.0,
          surface_dewpoint_c: 18.0,
          surface_pressure_mb: 1013.0,
          hpbl_m: 500.0,
          pwat_mm: 30.0,
          profile: []
        }
      })

      refute ScalarFile.exists?(vt)

      :ok = NotifyListener.handle_propagation_ready(vt)

      # Background Task → poll briefly until the scalar file lands.
      assert wait_until(fn -> ScalarFile.exists?(vt) end, 1_000),
             "expected ScalarFile to be materialized after handle_propagation_ready"
    end
  end

  describe "lifecycle" do
    test "init/1 schedules a :subscribe message and starts with a blank state" do
      assert {:ok, %{ref: nil, pid: nil}} = NotifyListener.init([])
      assert_receive :subscribe, 1_000
    end

    test "handle_info(:subscribe, _) succeeds against the real Repo" do
      # Happy-path smoke test — the live Repo config is valid in the
      # test environment, so Postgrex.Notifications.start_link should
      # connect and the handler should store {pid, ref} in state.
      state = %{ref: nil, pid: nil}
      assert {:noreply, %{pid: pid, ref: ref}} = NotifyListener.handle_info(:subscribe, state)
      assert is_pid(pid)
      assert is_reference(ref)

      # Tidy up the helper connection so it doesn't leak into later tests.
      GenServer.stop(pid)
    end
  end

  describe "handle_info/2 raw dispatch" do
    test "notification with a malformed payload is tolerated (no crash)" do
      # The GenServer callback must not raise even when PostgreSQL emits
      # a malformed payload. Direct-dispatch the callback to avoid
      # standing up a real Postgrex.Notifications subscriber.
      assert {:noreply, %{ref: nil, pid: nil}} =
               NotifyListener.handle_info(
                 {:notification, self(), make_ref(), "propagation_ready", "not-a-datetime"},
                 %{ref: nil, pid: nil}
               )
    end

    test "unrelated messages fall through without changing state" do
      state = %{ref: nil, pid: nil}
      assert {:noreply, ^state} = NotifyListener.handle_info(:something_else, state)
      assert {:noreply, ^state} = NotifyListener.handle_info({:EXIT, self(), :normal}, state)
    end
  end

  defp wait_until(fun, timeout_ms) do
    deadline = System.monotonic_time(:millisecond) + timeout_ms

    cond do
      fun.() -> true
      System.monotonic_time(:millisecond) >= deadline -> false
      true -> wait_until_step(fun, deadline)
    end
  end

  defp wait_until_step(fun, deadline) do
    Process.sleep(20)

    cond do
      fun.() -> true
      System.monotonic_time(:millisecond) >= deadline -> false
      true -> wait_until_step(fun, deadline)
    end
  end
end
