defmodule Microwaveprop.Propagation.ScoreCacheTest do
  use ExUnit.Case, async: false

  alias Microwaveprop.Propagation.ScoreCache

  setup do
    ScoreCache.clear()
    :ok
  end

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

    test "returns cached scores after put" do
      scores = [%{lat: 32.0, lon: -97.0, score: 75}]
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], scores)

      assert {:ok, [%{lat: 32.0, lon: -97.0, score: 75}]} =
               ScoreCache.fetch(10_000, ~U[2026-04-12 12:00:00Z])
    end

    test "isolates entries by band" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [%{lat: 1.0, lon: 1.0, score: 10}])
      ScoreCache.put(24_000, ~U[2026-04-12 12:00:00Z], [%{lat: 2.0, lon: 2.0, score: 20}])

      assert {:ok, [%{score: 10}]} = ScoreCache.fetch(10_000, ~U[2026-04-12 12:00:00Z])
      assert {:ok, [%{score: 20}]} = ScoreCache.fetch(24_000, ~U[2026-04-12 12:00:00Z])
    end

    test "isolates entries by valid_time" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [%{lat: 1.0, lon: 1.0, score: 10}])
      ScoreCache.put(10_000, ~U[2026-04-12 13:00:00Z], [%{lat: 2.0, lon: 2.0, score: 20}])

      assert {:ok, [%{score: 10}]} = ScoreCache.fetch(10_000, ~U[2026-04-12 12:00:00Z])
      assert {:ok, [%{score: 20}]} = ScoreCache.fetch(10_000, ~U[2026-04-12 13:00:00Z])
    end
  end

  describe "fetch_bounds/3" do
    setup do
      scores = [
        %{lat: 32.0, lon: -97.0, score: 75},
        %{lat: 40.0, lon: -74.0, score: 50},
        %{lat: 34.0, lon: -98.0, score: 60}
      ]

      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], scores)
      :ok
    end

    test "returns :miss when no entry for band/time" do
      bounds = %{"south" => 30.0, "north" => 50.0, "west" => -100.0, "east" => -70.0}
      assert ScoreCache.fetch_bounds(24_000, ~U[2026-04-12 12:00:00Z], bounds) == :miss
    end

    test "returns all scores when bounds is nil" do
      assert {:ok, all} = ScoreCache.fetch_bounds(10_000, ~U[2026-04-12 12:00:00Z], nil)
      assert length(all) == 3
    end

    test "returns only scores inside the bounds" do
      bounds = %{"south" => 31.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}

      assert {:ok, filtered} =
               ScoreCache.fetch_bounds(10_000, ~U[2026-04-12 12:00:00Z], bounds)

      assert length(filtered) == 2
      assert Enum.all?(filtered, fn %{lat: lat} -> lat >= 31.0 and lat <= 35.0 end)
    end

    test "includes scores exactly on the boundary" do
      bounds = %{"south" => 32.0, "north" => 32.0, "west" => -97.0, "east" => -97.0}

      assert {:ok, [%{lat: 32.0, lon: -97.0, score: 75}]} =
               ScoreCache.fetch_bounds(10_000, ~U[2026-04-12 12:00:00Z], bounds)
    end
  end

  describe "prune_older_than/1" do
    test "removes entries whose valid_time is strictly before the cutoff" do
      ScoreCache.put(10_000, ~U[2026-04-12 10:00:00Z], [%{lat: 1.0, lon: 1.0, score: 1}])
      ScoreCache.put(10_000, ~U[2026-04-12 14:00:00Z], [%{lat: 2.0, lon: 2.0, score: 2}])

      ScoreCache.prune_older_than(~U[2026-04-12 12:00:00Z])

      assert ScoreCache.fetch(10_000, ~U[2026-04-12 10:00:00Z]) == :miss
      assert {:ok, [%{score: 2}]} = ScoreCache.fetch(10_000, ~U[2026-04-12 14:00:00Z])
    end

    test "keeps entries whose valid_time equals the cutoff" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [%{lat: 1.0, lon: 1.0, score: 1}])
      ScoreCache.prune_older_than(~U[2026-04-12 12:00:00Z])
      assert {:ok, [%{score: 1}]} = ScoreCache.fetch(10_000, ~U[2026-04-12 12:00:00Z])
    end
  end

  describe "fetch_point/4" do
    test "returns :miss when nothing cached" do
      assert ScoreCache.fetch_point(10_000, ~U[2026-04-12 12:00:00Z], 32.0, -97.0) == :miss
    end

    test "returns :miss when the point is not in the cached grid" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [
        %{lat: 32.0, lon: -97.0, score: 75}
      ])

      assert ScoreCache.fetch_point(10_000, ~U[2026-04-12 12:00:00Z], 40.0, -74.0) == :miss
    end

    test "returns the score for a cached point" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [
        %{lat: 32.0, lon: -97.0, score: 75},
        %{lat: 33.0, lon: -97.0, score: 80}
      ])

      assert {:ok, 75} = ScoreCache.fetch_point(10_000, ~U[2026-04-12 12:00:00Z], 32.0, -97.0)
      assert {:ok, 80} = ScoreCache.fetch_point(10_000, ~U[2026-04-12 12:00:00Z], 33.0, -97.0)
    end
  end

  describe "valid_times/1" do
    test "returns empty list when nothing cached" do
      assert ScoreCache.valid_times(10_000) == []
    end

    test "returns sorted valid_times for a band" do
      ScoreCache.put(10_000, ~U[2026-04-12 14:00:00Z], [])
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [])
      ScoreCache.put(10_000, ~U[2026-04-12 13:00:00Z], [])

      assert ScoreCache.valid_times(10_000) == [
               ~U[2026-04-12 12:00:00Z],
               ~U[2026-04-12 13:00:00Z],
               ~U[2026-04-12 14:00:00Z]
             ]
    end

    test "only returns times for the requested band" do
      ScoreCache.put(10_000, ~U[2026-04-12 12:00:00Z], [])
      ScoreCache.put(24_000, ~U[2026-04-12 13:00:00Z], [])

      assert ScoreCache.valid_times(10_000) == [~U[2026-04-12 12:00:00Z]]
      assert ScoreCache.valid_times(24_000) == [~U[2026-04-12 13:00:00Z]]
    end
  end

  describe "broadcast_put/3" do
    test "populates the cache on the local node" do
      scores = [%{lat: 32.0, lon: -97.0, score: 80}]
      ScoreCache.broadcast_put(10_000, ~U[2026-04-12 12:00:00Z], scores)
      ScoreCache.sync()

      assert {:ok, [%{lat: 32.0, lon: -97.0, score: 80}]} =
               ScoreCache.fetch(10_000, ~U[2026-04-12 12:00:00Z])
    end
  end
end
