defmodule Aprsme.SpatialPubSubTest do
  use ExUnit.Case, async: false

  alias Aprsme.SpatialPubSub

  describe "viewport registration" do
    test "unbounded clients receive positioned packets" do
      client_id = "test_unbounded_#{:rand.uniform(100_000)}"
      assert {:ok, topic} = SpatialPubSub.register_unbounded(client_id)
      :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)

      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5, sender: "ANYWHERE"})
      assert_receive {:spatial_packet, %{sender: "ANYWHERE"}}, 1_000

      SpatialPubSub.unregister_client(client_id)
    end

    test "a process with bounded and unbounded registrations receives a packet once" do
      bounded_id = "test_bounded_#{:rand.uniform(100_000)}"
      unbounded_id = "test_unbounded_#{:rand.uniform(100_000)}"
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}

      assert {:ok, topic} = SpatialPubSub.register_viewport(bounded_id, bounds)
      assert {:ok, ^topic} = SpatialPubSub.register_unbounded(unbounded_id)
      :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)

      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5, sender: "ONCE"})
      assert_receive {:spatial_packet, %{sender: "ONCE"}}, 1_000
      refute_receive {:spatial_packet, %{sender: "ONCE"}}, 100

      SpatialPubSub.unregister_client(bounded_id)
      SpatialPubSub.unregister_client(unbounded_id)
    end

    test "date-line-crossing viewport registers without timeout" do
      bounds = %{north: 10.0, south: -10.0, east: -170.0, west: 170.0}
      client_id = "test_dateline_#{:rand.uniform(100_000)}"

      # Before the fix, west=170 east=-170 generated ~341 grid cells.
      # Now it correctly generates ~21 cells.
      assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)

      SpatialPubSub.unregister_client(client_id)
    end

    test "normal viewport registers successfully" do
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      client_id = "test_normal_#{:rand.uniform(100_000)}"

      assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)

      SpatialPubSub.unregister_client(client_id)
    end

    test "update_viewport succeeds after registration" do
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      new_bounds = %{north: 42.0, south: 41.0, east: -72.0, west: -73.0}
      client_id = "test_update_#{:rand.uniform(100_000)}"

      {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
      assert :ok = SpatialPubSub.update_viewport(client_id, new_bounds)

      SpatialPubSub.unregister_client(client_id)
    end

    test "re-registering an existing client replaces the old viewport without inflating counts" do
      client_id = "test_reregister_#{:rand.uniform(100_000)}"
      bounds_1 = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      bounds_2 = %{north: 36.0, south: 35.0, east: -79.0, west: -80.0}

      before_stats = SpatialPubSub.get_stats()

      assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds_1)
      mid_stats = SpatialPubSub.get_stats()
      assert mid_stats.clients_count == before_stats.clients_count + 1

      assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds_2)
      after_stats = SpatialPubSub.get_stats()

      assert after_stats.clients_count == before_stats.clients_count + 1

      state = :sys.get_state(SpatialPubSub)
      assert state.clients[client_id].bounds == bounds_2

      SpatialPubSub.unregister_client(client_id)
    end
  end

  describe "duplicate packet prevention" do
    test "does not subscribe to postgres PubSub topic" do
      # SpatialPubSub used to subscribe to "postgres:aprsme_packets" causing
      # duplicate broadcasts (once from PacketConsumer direct cast, once from PubSub).
      # Verify it no longer subscribes.
      # The SpatialPubSub process should NOT be in the subscriber list for postgres topic
      spatial_pid = Process.whereis(SpatialPubSub)

      assert Map.has_key?(SpatialPubSub.get_stats(), :total_packets)

      subscribed? =
        Aprsme.PubSub
        |> Registry.lookup("postgres:aprsme_packets")
        |> Enum.any?(fn {pid, _} -> pid == spatial_pid end)

      refute subscribed?, "SpatialPubSub should not subscribe to postgres:aprsme_packets"
    end
  end

  describe "stats" do
    test "get_stats returns expected keys" do
      stats = SpatialPubSub.get_stats()

      assert Map.has_key?(stats, :total_packets)
      assert Map.has_key?(stats, :grid_cells)
    end

    test "grid cells increase after registering viewport" do
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      client_id = "test_stats_#{:rand.uniform(100_000)}"

      before_stats = SpatialPubSub.get_stats()
      {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
      after_stats = SpatialPubSub.get_stats()

      assert after_stats.grid_cells >= before_stats.grid_cells

      SpatialPubSub.unregister_client(client_id)
    end
  end

  describe "broadcast_packet" do
    test "broadcasts to clients whose viewport contains the packet location" do
      client_id = "test_bc_#{:rand.uniform(100_000)}"
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      {:ok, topic} = SpatialPubSub.register_viewport(client_id, bounds)

      Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)

      # Broadcast a packet inside the viewport.
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5, sender: "IN1"})

      assert_receive {:spatial_packet, %{sender: "IN1"}}, 1_000

      SpatialPubSub.unregister_client(client_id)
    end

    test "does not broadcast to clients outside the viewport" do
      client_id = "test_outside_#{:rand.uniform(100_000)}"
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      {:ok, topic} = SpatialPubSub.register_viewport(client_id, bounds)

      Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)

      # Broadcast far outside bounds.
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 0.0, lon: 0.0, sender: "OUT1"})
      refute_receive {:spatial_packet, _}, 100

      SpatialPubSub.unregister_client(client_id)
    end

    test "ignores packets with missing or invalid location" do
      # Packet with nil coords should not crash.
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: nil, lon: nil})
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: "bad", lon: "bad"})
      assert :ok = SpatialPubSub.broadcast_packet(%{})
    end

    test "binary-string coordinates in bounds are parsed" do
      client_id = "test_strbounds_#{:rand.uniform(100_000)}"
      bounds = %{north: "41.0", south: "40.0", east: "-73.0", west: "-74.0"}
      assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
      SpatialPubSub.unregister_client(client_id)
    end
  end

  describe "client lifecycle" do
    test "unregister removes client from spatial index" do
      client_id = "test_unreg_#{:rand.uniform(100_000)}"
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)

      before_stats = SpatialPubSub.get_stats()
      SpatialPubSub.unregister_client(client_id)
      # Give cast time to process
      _ = SpatialPubSub.get_stats()
      after_stats = SpatialPubSub.get_stats()

      assert after_stats.clients_count <= before_stats.clients_count
    end

    test "update_viewport returns error for unregistered client" do
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      result = SpatialPubSub.update_viewport("never_registered_#{:rand.uniform(100_000)}", bounds)
      assert result == {:error, :not_registered}
    end
  end

  describe "ensure_float branches via register_viewport" do
    test "accepts integer bounds (ensure_float integer clause)" do
      client = "int-bounds-#{System.unique_integer([:positive])}"

      bounds = %{north: 41, south: 40, east: -73, west: -74}
      assert {:ok, _topic} = SpatialPubSub.register_viewport(client, bounds)
      SpatialPubSub.unregister_client(client)
    end

    test "ensure_float fallback handles bogus atom values without crashing" do
      client = "bogus-#{System.unique_integer([:positive])}"
      # Atoms aren't covered by the binary/integer/float clauses; fall through to 0.0.
      bounds = %{north: :unknown, south: :unknown, east: :unknown, west: :unknown}
      assert {:ok, _topic} = SpatialPubSub.register_viewport(client, bounds)
      SpatialPubSub.unregister_client(client)
    end

    test "registering the same client_id twice replaces the prior entry" do
      client = "replace-#{System.unique_integer([:positive])}"

      bounds_a = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      bounds_b = %{north: 51.0, south: 50.0, east: -83.0, west: -84.0}

      assert {:ok, _} = SpatialPubSub.register_viewport(client, bounds_a)
      assert {:ok, _} = SpatialPubSub.register_viewport(client, bounds_b)

      SpatialPubSub.unregister_client(client)
    end

    test "registering two clients in the same grid cell exercises the existing-set branch" do
      a = "share-a-#{System.unique_integer([:positive])}"
      b = "share-b-#{System.unique_integer([:positive])}"

      # Same exact bounds → both clients land in the same grid cell.
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}

      assert {:ok, _} = SpatialPubSub.register_viewport(a, bounds)
      assert {:ok, _} = SpatialPubSub.register_viewport(b, bounds)

      SpatialPubSub.unregister_client(a)
      SpatialPubSub.unregister_client(b)
    end
  end

  describe "broadcast_packet across the international date line" do
    test "matches a client whose bounds wrap from positive to negative longitude" do
      client = "dateline-#{System.unique_integer([:positive])}"

      # west > east → bounds cross the date line.
      bounds = %{north: 60.0, south: 30.0, east: -170.0, west: 170.0}
      {:ok, _topic} = SpatialPubSub.register_viewport(client, bounds)

      # A point west of the date line (lon: -179) should match via
      # `lon <= e` half of the wrap-aware comparison.
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 45.0, lon: -179.0})

      # A point east of the date line (lon: 175) should match via the
      # `lon >= w` half.
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 45.0, lon: 175.0})

      SpatialPubSub.unregister_client(client)
    end
  end

  describe "extract_location with no coordinates" do
    test "broadcast_packet ignores packets without lat/lon without crashing" do
      assert :ok = SpatialPubSub.broadcast_packet(%{some: "field"})
    end
  end

  describe "DOWN handler removes the client" do
    test "monitored client process exiting unregisters the client" do
      task =
        Task.async(fn ->
          receive do
            :stop -> :ok
          after
            2_000 -> :ok
          end
        end)

      client = "down-#{System.unique_integer([:positive])}"

      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
      {:ok, _} = SpatialPubSub |> GenServer.call({:register_viewport, client, bounds}, 5_000) |> tap(fn _ -> :ok end)
      _ = task

      # The above register_viewport monitored the test process; force the
      # task to exit and observe stats remain consistent.
      send(task.pid, :stop)
      _ = Task.shutdown(task, :brutal_kill)

      Process.sleep(50)
      assert Map.has_key?(SpatialPubSub.get_stats(), :total_packets)

      SpatialPubSub.unregister_client(client)
    end
  end

  describe "when the server is not running" do
    setup do
      pid = Process.whereis(SpatialPubSub)

      if pid do
        GenServer.stop(pid)
      end

      on_exit(fn ->
        if !Process.whereis(SpatialPubSub) do
          start_supervised!({SpatialPubSub, []})
        end
      end)

      :ok
    end

    test "public APIs return safe defaults instead of crashing" do
      bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}

      assert {:error, :not_running} = SpatialPubSub.register_viewport("missing", bounds)
      assert {:error, :not_running} = SpatialPubSub.update_viewport("missing", bounds)
      assert :ok = SpatialPubSub.unregister_client("missing")
      assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5})
      assert :ok = SpatialPubSub.start_telemetry_reporting()

      assert SpatialPubSub.get_stats() == %{
               total_broadcasts: 0,
               filtered_broadcasts: 0,
               total_packets: 0,
               clients_count: 0,
               grid_cells: 0,
               avg_clients_per_cell: 0.0
             }
    end
  end
end
