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 describe "prune_older_than/1" do test "removes expired valid_times" 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}]) past = ~U[2026-04-12 10:30:00Z] removed = GridCache.prune_older_than(past) assert removed == 1 assert GridCache.fetch(~U[2026-04-12 10:00:00Z]) == :miss assert {:ok, _} = GridCache.fetch(~U[2026-04-12 11:00:00Z]) end end end