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

  alias Microwaveprop.Weather.GridCache

  setup do
    GridCache.clear()
    :ok
  end

  describe "fetch/1" do
    test "returns :miss when nothing is cached" do
      assert GridCache.fetch(~U[2026-04-12 12:00:00Z]) == :miss
    end

    test "returns cached rows after put" do
      rows = [%{lat: 32.0, lon: -97.0, temperature: 25.0}]
      GridCache.put(~U[2026-04-12 12:00:00Z], rows)
      assert {:ok, [%{lat: 32.0, lon: -97.0}]} = GridCache.fetch(~U[2026-04-12 12:00:00Z])
    end
  end

  describe "fetch_bounds/2" do
    setup do
      rows = [
        %{lat: 32.0, lon: -97.0, temperature: 25.0},
        %{lat: 40.0, lon: -74.0, temperature: 20.0},
        %{lat: 34.0, lon: -98.0, temperature: 28.0}
      ]

      GridCache.put(~U[2026-04-12 12:00:00Z], rows)
      :ok
    end

    test "returns all rows when bounds are nil" do
      assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], nil)
      assert length(list) == 3
    end

    test "returns only rows inside the bounds" do
      bounds = %{"south" => 31.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}
      assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], bounds)
      assert length(list) == 2
    end

    test "returns :miss when the valid_time is not cached" do
      bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
      assert GridCache.fetch_bounds(~U[2099-01-01 00:00:00Z], bounds) == :miss
    end
  end

  describe "fetch_point/3" do
    setup do
      rows = [
        %{lat: 32.0, lon: -97.0, temperature: 25.0},
        %{lat: 33.0, lon: -97.0, temperature: 27.0}
      ]

      GridCache.put(~U[2026-04-12 12:00:00Z], rows)
      :ok
    end

    test "returns the cached row for a known point" do
      assert {:ok, %{temperature: 25.0}} =
               GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 32.0, -97.0)
    end

    test "returns :miss for an unknown point" do
      assert GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 40.0, -74.0) == :miss
    end
  end

  describe "claim_fill/1 and release_fill/1" do
    @valid_time ~U[2026-04-21 12:00:00Z]

    test "first claimer wins, subsequent claimers see :in_progress" do
      assert GridCache.claim_fill(@valid_time) == true
      assert GridCache.claim_fill(@valid_time) == false
    end

    test "release_fill allows a re-claim" do
      assert GridCache.claim_fill(@valid_time) == true
      :ok = GridCache.release_fill(@valid_time)
      assert GridCache.claim_fill(@valid_time) == true
    end

    test "lock is auto-released when the claimer process crashes" do
      parent = self()

      {:ok, claimer} =
        Task.start(fn ->
          true = GridCache.claim_fill(@valid_time)
          send(parent, :claimed)
          # Block until we're killed
          Process.sleep(:infinity)
        end)

      receive do
        :claimed -> :ok
      after
        1000 -> flunk("claimer never signaled")
      end

      # Lock is held by the crashed process
      assert GridCache.claim_fill(@valid_time) == false

      # Kill the claimer
      ref = Process.monitor(claimer)
      Process.exit(claimer, :kill)

      receive do
        {:DOWN, ^ref, :process, ^claimer, _} -> :ok
      after
        1000 -> flunk("claimer didn't die")
      end

      # Flush the GenServer so the :DOWN handler runs before we re-probe ETS.
      :ok = GridCache.sync()

      assert GridCache.claim_fill(@valid_time) == true
    end
  end

  describe "lookup telemetry" do
    @event [:microwaveprop, :weather, :grid_cache, :lookup]

    setup do
      test_pid = self()
      handler_id = {__MODULE__, :grid_cache_lookup, System.unique_integer([:positive])}

      :telemetry.attach(
        handler_id,
        @event,
        fn event, measurements, metadata, _config ->
          send(test_pid, {:telemetry, event, measurements, metadata})
        end,
        nil
      )

      on_exit(fn -> :telemetry.detach(handler_id) end)
      :ok
    end

    test "fetch/1 emits hit: true when the valid_time is cached" do
      GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
      {:ok, _} = GridCache.fetch(~U[2026-04-12 12:00:00Z])
      assert_receive {:telemetry, @event, %{}, %{hit: true}}
    end

    test "fetch/1 emits hit: false when the valid_time is absent" do
      :miss = GridCache.fetch(~U[2099-01-01 00:00:00Z])
      assert_receive {:telemetry, @event, %{}, %{hit: false}}
    end

    test "fetch_bounds/2 emits hit: true on a cached valid_time" do
      GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
      bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
      {:ok, _} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], bounds)
      assert_receive {:telemetry, @event, %{}, %{hit: true}}
    end

    test "fetch_bounds/2 emits hit: false when the valid_time is absent" do
      bounds = %{"south" => 0.0, "north" => 90.0, "west" => -180.0, "east" => 0.0}
      :miss = GridCache.fetch_bounds(~U[2099-01-01 00:00:00Z], bounds)
      assert_receive {:telemetry, @event, %{}, %{hit: false}}
    end

    test "fetch_point/3 emits hit: true when the point exists" do
      GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
      {:ok, _} = GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 32.0, -97.0)
      assert_receive {:telemetry, @event, %{}, %{hit: true}}
    end

    test "fetch_point/3 emits hit: false when the valid_time is missing" do
      :miss = GridCache.fetch_point(~U[2099-01-01 00:00:00Z], 1.0, 2.0)
      assert_receive {:telemetry, @event, %{}, %{hit: false}}
    end

    test "fetch_point/3 emits hit: false when the point is missing from a cached grid" do
      GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
      :miss = GridCache.fetch_point(~U[2026-04-12 12:00:00Z], 40.0, -74.0)
      assert_receive {:telemetry, @event, %{}, %{hit: false}}
    end
  end

  describe "latest_valid_time/0" do
    test "returns the most recent cached valid_time" do
      GridCache.put(~U[2026-04-12 10:00:00Z], [])
      GridCache.put(~U[2026-04-12 14:00:00Z], [])
      GridCache.put(~U[2026-04-12 12:00:00Z], [])
      assert GridCache.latest_valid_time() == ~U[2026-04-12 14:00:00Z]
    end

    test "returns nil when nothing is cached" do
      assert GridCache.latest_valid_time() == nil
    end
  end

  describe "prune_keep_latest/1" do
    test "drops oldest valid_times beyond the cap" do
      GridCache.put(~U[2026-04-12 10:00:00Z], [%{lat: 32.0, lon: -97.0}])
      GridCache.put(~U[2026-04-12 11:00:00Z], [%{lat: 32.0, lon: -97.0}])
      GridCache.put(~U[2026-04-12 12:00:00Z], [%{lat: 32.0, lon: -97.0}])
      GridCache.put(~U[2026-04-12 13:00:00Z], [%{lat: 32.0, lon: -97.0}])

      removed = GridCache.prune_keep_latest(2)
      assert removed == 2

      assert GridCache.fetch(~U[2026-04-12 10:00:00Z]) == :miss
      assert GridCache.fetch(~U[2026-04-12 11:00:00Z]) == :miss
      assert {:ok, _} = GridCache.fetch(~U[2026-04-12 12:00:00Z])
      assert {:ok, _} = GridCache.fetch(~U[2026-04-12 13:00:00Z])
    end

    test "is a no-op when the cap is not exceeded" do
      GridCache.put(~U[2026-04-12 10:00:00Z], [%{lat: 32.0, lon: -97.0}])
      GridCache.put(~U[2026-04-12 11:00:00Z], [%{lat: 32.0, lon: -97.0}])

      assert GridCache.prune_keep_latest(5) == 0
      assert {:ok, _} = GridCache.fetch(~U[2026-04-12 10:00:00Z])
      assert {:ok, _} = GridCache.fetch(~U[2026-04-12 11:00:00Z])
    end
  end
end
