defmodule AprsmeWeb.MapLive.IntegrationTest do
  use AprsmeWeb.ConnCase

  import Aprsme.PacketsFixtures
  import Phoenix.LiveViewTest

  describe "MapLive.Index with real packet data" do
    test "renders a map with pre-existing packets and simulates bounds_changed", %{conn: conn} do
      # Seed some packets in the bounds we'll scroll to.
      now = DateTime.utc_now()

      for i <- 1..3 do
        packet_fixture(%{
          sender: "INT#{i}",
          base_callsign: "INT#{i}",
          ssid: "0",
          received_at: DateTime.add(now, -i * 60, :second),
          lat: Decimal.new("#{40 + i * 0.01}"),
          lon: Decimal.new("#{-74 - i * 0.01}"),
          has_position: true,
          path: "WIDE1-1"
        })
      end

      {:ok, view, _html} = live(conn, "/?lat=40.0&lng=-74.0&z=11", on_error: :warn)

      # Fire a bounds_changed event covering the seeded region.
      bounds = %{
        "bounds" => %{
          "north" => 41.0,
          "south" => 39.5,
          "east" => -73.0,
          "west" => -74.5
        }
      }

      assert render_hook(view, "bounds_changed", bounds)
      assert render_hook(view, "map_ready", %{})

      # Allow async historical loading to run.
      Process.sleep(100)
      html = render(view)
      assert html =~ "aprs-map"
    end

    test "tracks a callsign that has a real packet", %{conn: conn} do
      _packet =
        packet_fixture(%{
          sender: "TRACKME",
          base_callsign: "TRACKME",
          ssid: "0",
          received_at: DateTime.utc_now(),
          lat: Decimal.new("33.0"),
          lon: Decimal.new("-96.5"),
          has_position: true,
          path: "WIDE1-1"
        })

      {:ok, view, _html} = live(conn, "/?call=TRACKME", on_error: :warn)
      assert render_hook(view, "map_ready", %{})
      Process.sleep(100)
      html = render(view)
      assert html =~ "aprs-map"
    end

    test "callsign path route loads the tracked callsign", %{conn: conn} do
      _packet =
        packet_fixture(%{
          sender: "PATH1",
          base_callsign: "PATH1",
          ssid: "0",
          received_at: DateTime.utc_now(),
          lat: Decimal.new("35.0"),
          lon: Decimal.new("-90.0"),
          has_position: true,
          path: "WIDE1-1"
        })

      {:ok, _view, html} = live(conn, "/PATH1", on_error: :warn)
      assert html =~ "aprs-map"
    end

    test "spatial_packet arriving for an in-bounds callsign triggers display", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)

      render_hook(view, "bounds_changed", %{
        "bounds" => %{
          "north" => 34.0,
          "south" => 32.0,
          "east" => -95.0,
          "west" => -97.0
        }
      })

      packet = %{
        id: "sp-1",
        sender: "SP-1",
        base_callsign: "SP",
        ssid: "1",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: "WIDE1-1",
        comment: "in bounds"
      }

      send(view.pid, {:spatial_packet, packet})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "handles a batch of packets including out-of-bounds ones", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)

      render_hook(view, "bounds_changed", %{
        "bounds" => %{
          "north" => 34.0,
          "south" => 32.0,
          "east" => -95.0,
          "west" => -97.0
        }
      })

      packets = [
        %{
          id: "b-1",
          sender: "IN-1",
          base_callsign: "IN",
          ssid: "1",
          lat: 33.5,
          lon: -96.0,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        },
        %{
          id: "b-2",
          sender: "OUT-1",
          base_callsign: "OUT",
          ssid: "1",
          lat: 50.0,
          lon: -80.0,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        }
      ]

      send(view.pid, {:packet_batch, packets})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end
  end

  describe "MapLive handle_event coverage for less-exercised events" do
    test "popup_closed, get_assigns, set_slideover_state, request_geolocation, geolocation_error", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      assert render_hook(view, "popup_closed", %{})
      assert render_hook(view, "set_slideover_state", %{"open" => false})
      assert render_hook(view, "set_slideover_state", %{"open" => true})
      assert render_hook(view, "request_geolocation", %{})
      assert render_hook(view, "geolocation_error", %{"error" => "denied"})
      assert render_hook(view, "toggle_slideover", %{})
    end

    test "marker_hover_start and marker_hover_end exercise hover handlers", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      assert render_hook(view, "marker_hover_start", %{
               "id" => "m-1",
               "path" => "WIDE1-1",
               "lat" => 33.0,
               "lng" => -96.0
             })

      assert render_hook(view, "marker_hover_end", %{})
    end

    test "search_callsign with empty input no-ops", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      assert render_hook(view, "search_callsign", %{"callsign" => "   "})
    end

    test "track_callsign with empty input clears tracking", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?call=ZZZ", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Empty callsign goes through the clear-tracking branch.
      assert render_hook(view, "track_callsign", %{"callsign" => ""})
    end

    test "track_callsign with non-empty callsign that has a packet zooms and adds marker", %{conn: conn} do
      # Seed a real packet so track_callsign exercises the latest_packet branch.
      packet_fixture(%{
        sender: "TRACKEV",
        base_callsign: "TRACKEV",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("45.0"),
        lon: Decimal.new("-93.0"),
        has_position: true,
        path: "WIDE1-1"
      })

      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      render_hook(view, "map_ready", %{})
      assert render_hook(view, "track_callsign", %{"callsign" => "TRACKEV"})
    end

    test "marker_hover_start with a path triggers RF path drawing", %{conn: conn} do
      # Seed packets that match the path stations so RF path resolution finds them.
      packet_fixture(%{
        sender: "PATHST1",
        base_callsign: "PATHST1",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("33.5"),
        lon: Decimal.new("-96.5"),
        has_position: true,
        path: ""
      })

      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      assert render_hook(view, "marker_hover_start", %{
               "id" => "m-rf",
               "path" => "PATHST1*,WIDE1-1",
               "lat" => 33.0,
               "lng" => -96.0
             })
    end

    test "clear_tracking event clears the tracked callsign", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?call=TRACK1", on_error: :warn)
      render_hook(view, "map_ready", %{})
      assert render_hook(view, "clear_tracking", %{})
    end

    test "update_callsign updates overlay_callsign", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      assert render_hook(view, "update_callsign", %{"callsign" => "AB1XYZ"})
    end

    test "update_trail_duration changes the threshold", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})
      assert render_hook(view, "update_trail_duration", %{"trail_duration" => "6"})
    end

    test "update_historical_hours changes assigns", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})
      assert render_hook(view, "update_historical_hours", %{"historical_hours" => "3"})
    end

    test "locate_me, set_location, clear_and_reload_markers events", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      assert render_hook(view, "locate_me", %{})
      assert render_hook(view, "set_location", %{"lat" => 33.5, "lng" => -96.5})
      assert render_hook(view, "clear_and_reload_markers", %{})
    end

    test "update_map_state with center and zoom", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      assert render_hook(view, "update_map_state", %{
               "center" => %{"lat" => 34.0, "lng" => -95.0},
               "zoom" => 11
             })
    end

    test "marker_clicked event", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      assert render_hook(view, "marker_clicked", %{})
    end

    test "error_boundary_triggered event logs without crashing", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)

      assert render_hook(view, "error_boundary_triggered", %{
               "message" => "test error",
               "stack" => "test stack",
               "component_id" => "comp-1"
             })
    end
  end

  describe "MapLive handle_info coverage" do
    test ":cleanup_old_packets, :reload_historical_packets do not crash", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      send(view.pid, :cleanup_old_packets)
      send(view.pid, :reload_historical_packets)
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test ":clear_rf_path message clears the RF path", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      send(view.pid, :clear_rf_path)
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:new_deployment, _} message updates deployed_at", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      send(view.pid, {:new_deployment, %{deployed_at: DateTime.utc_now()}})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:drain_connections, n} message processes drain", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      send(view.pid, {:drain_connections, 0})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test ":initialize_replay message reaches handle_info_initialize_replay", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})
      send(view.pid, :initialize_replay)
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:load_rf_path_station_packets, _} message hits get_latest_packets_for_callsigns", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      send(view.pid, {:load_rf_path_station_packets, ["FOO", "BAR"]})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:show_error, message} message sets a flash", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      send(view.pid, {:show_error, "boom"})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:load_historical_batch, _} backward-compat message", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})
      send(view.pid, {:load_historical_batch, 0})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:load_historical_batch, _, generation} matching current generation", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Force loading_generation to a known value so the matching-branch
      # (line 957) is exercised.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket
        %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :loading_generation, 7)}}
      end)

      send(view.pid, {:load_historical_batch, 0, 7})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:load_historical_batch, _, stale_gen} with stale generation is ignored", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Use a wildly mismatched generation — the handler should ignore it.
      send(view.pid, {:load_historical_batch, 0, 99_999})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:historical_loading_timeout, _} for stale generation no-ops", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      send(view.pid, {:historical_loading_timeout, 99_999})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "{:process_pending_bounds} message", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      send(view.pid, {:process_pending_bounds})
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "Logger.debug-laden bounds-update path runs format strings at :debug level", %{conn: conn} do
      original_level = Logger.level()
      Logger.configure(level: :debug)

      try do
        {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
        render_hook(view, "map_ready", %{})

        # Send bounds events so that Logger.debug calls in process_bounds_update,
        # handle_info_process_bounds_update, and process_bounds_from_params get
        # their string interpolations evaluated.
        send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}})

        render_hook(view, "bounds_changed", %{
          "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0}
        })

        render_hook(view, "update_map_state", %{
          "center" => %{"lat" => 33.5, "lng" => -96.5},
          "zoom" => 11,
          "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0}
        })

        Process.sleep(50)
      after
        Logger.configure(level: original_level)
      end
    end

    test "postgres_packet via PubSub", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      packet = %{
        id: "pg-1",
        sender: "PG-1",
        base_callsign: "PG",
        ssid: "1",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: "WIDE1-1"
      }

      send(view.pid, {:postgres_packet, packet})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "{:DOWN, ref, _, batcher_pid, _} restarts the PacketBatcher", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)

      %{socket: %{assigns: %{batcher_pid: original_pid}}} = :sys.get_state(view.pid)

      # Send a fake DOWN message to simulate the batcher dying.
      send(view.pid, {:DOWN, make_ref(), :process, original_pid, :test_kill})
      Process.sleep(30)

      # The handle_info DOWN handler should have spawned a new batcher.
      %{socket: %{assigns: %{batcher_pid: new_pid}}} = :sys.get_state(view.pid)
      assert is_pid(new_pid)
    end

    test "{:process_pending_bounds} with pending_bounds set triggers handle_bounds_update", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:pending_bounds, %{north: 35.0, south: 31.0, east: -94.0, west: -98.0})
                  |> Map.put(:historical_loading, false)
            }
        }
      end)

      send(view.pid, {:process_pending_bounds})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "stale bounds-update Logger.debug branch with :debug log level", %{conn: conn} do
      original_level = Logger.level()
      Logger.configure(level: :debug)

      try do
        {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)

        # The LV channel state nests the socket under :socket. Set pending_bounds
        # via :sys.replace_state with that wrapper structure.
        :sys.replace_state(view.pid, fn channel_state ->
          %{assigns: assigns} = channel_state.socket

          %{
            channel_state
            | socket: %{
                channel_state.socket
                | assigns: Map.put(assigns, :pending_bounds, %{north: 99.0, south: 0.0, east: 1.0, west: 0.0})
              }
          }
        end)

        # Send a different bounds — should hit the "stale" Logger.debug branch
        # (line 1003 in index.ex), evaluating the format string at :debug level.
        send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}})
        Process.sleep(30)

        assert render(view) =~ "aprs-map"
      after
        Logger.configure(level: original_level)
      end
    end

    test "tracked-callsign postgres_packet path with batcher_pid forced nil", %{conn: conn} do
      # Force batcher_pid to nil so {:postgres_packet, _} flows through
      # handle_info_postgres_packet (lines 1053+).
      {:ok, view, _html} = live(conn, "/?call=PGTRK", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket
        %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}}
      end)

      # Send a packet with sender matching tracked callsign — exercises the
      # tracked-callsign branch in handle_info_postgres_packet.
      packet = %{
        id: "pgtrk-1",
        sender: "PGTRK",
        base_callsign: "PGTRK",
        ssid: "0",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: "WIDE1-1"
      }

      send(view.pid, {:postgres_packet, packet})
      Process.sleep(50)

      # Send a packet with a different sender — exercises the no-match branch.
      send(view.pid, {:postgres_packet, %{packet | sender: "OTHER", base_callsign: "OTHER"}})
      Process.sleep(50)

      assert render(view) =~ "aprs-map"
    end

    test "untracked postgres_packet path with batcher_pid nil hits handle_info_postgres_packet else branch", %{
      conn: conn
    } do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket
        %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}}
      end)

      packet = %{
        id: "untracked-1",
        sender: "UNT",
        base_callsign: "UNT",
        ssid: "1",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: "WIDE1-1"
      }

      send(view.pid, {:postgres_packet, packet})
      Process.sleep(50)

      assert render(view) =~ "aprs-map"
    end

    test "spatial_packet path with batcher_pid nil hits handle_info_postgres_packet branch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket
        %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}}
      end)

      packet = %{
        id: "spatial-x",
        sender: "SPX",
        base_callsign: "SPX",
        ssid: "1",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: "WIDE1-1"
      }

      send(view.pid, {:spatial_packet, packet})
      Process.sleep(50)
      send(view.pid, {:spatial_packet, packet})
      Process.sleep(50)

      assert render(view) =~ "aprs-map"
    end

    test "tracked-callsign positionless packets with NaiveDateTime received_at hits newer_packet? NaiveDateTime branch",
         %{
           conn: conn
         } do
      {:ok, view, _html} = live(conn, "/?call=NAIV&lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Pre-load tracked latest packet without position with a NaiveDateTime.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        current = %{
          sender: "NAIV",
          base_callsign: "NAIV",
          ssid: "0",
          lat: nil,
          lon: nil,
          has_position: false,
          received_at: ~N[2026-01-01 00:00:00]
        }

        %{
          channel_state
          | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)}
        }
      end)

      naive_packet = %{
        id: "naiv-1",
        sender: "NAIV",
        base_callsign: "NAIV",
        ssid: "0",
        lat: nil,
        lon: nil,
        has_position: false,
        received_at: ~N[2026-06-01 00:00:00],
        path: ""
      }

      send(view.pid, {:packet_batch, [naive_packet]})
      Process.sleep(50)

      # Now exercise the unrelated-types fallback (DateTime vs nil).
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        current = %{
          sender: "NAIV",
          base_callsign: "NAIV",
          ssid: "0",
          lat: nil,
          lon: nil,
          has_position: false,
          received_at: nil
        }

        %{
          channel_state
          | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)}
        }
      end)

      send(view.pid, {:packet_batch, [Map.put(naive_packet, :received_at, nil)]})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "clear_and_reload_markers with tracked callsign + visible packets exercises filter_with_tracked", %{conn: conn} do
      packet_fixture(%{
        sender: "CRMV",
        base_callsign: "CRMV",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("33.0"),
        lon: Decimal.new("-96.0"),
        has_position: true,
        path: "WIDE1-1"
      })

      {:ok, view, _html} = live(conn, "/?call=CRMV&lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Pre-populate visible_packets and tracked_callsign_latest_packet so the
      # clear_and_reload handler exercises the filter_with_tracked path.
      tracked = %{
        sender: "CRMV",
        base_callsign: "CRMV",
        ssid: "0",
        lat: 33.0,
        lon: -96.0,
        has_position: true,
        received_at: DateTime.utc_now()
      }

      visible = %{
        "CRMV-0" => tracked,
        "OTHER-0" => %{
          sender: "OTHER",
          base_callsign: "OTHER",
          ssid: "0",
          lat: 33.5,
          lon: -96.5,
          has_position: true,
          received_at: DateTime.utc_now()
        }
      }

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:visible_packets, visible)
                  |> Map.put(:tracked_callsign, "CRMV")
                  |> Map.put(:tracked_callsign_latest_packet, tracked)
            }
        }
      end)

      assert render_hook(view, "clear_and_reload_markers", %{})

      # Also at low zoom — exercises the heat map branch.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket
        %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :map_zoom, 6)}}
      end)

      assert render_hook(view, "clear_and_reload_markers", %{})
    end

    test "{:packet_batch, _} with out-of-bounds known marker triggers remove_markers_if_needed branch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      render_hook(view, "bounds_changed", %{
        "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0}
      })

      # Pre-populate visible_packets with a marker we'll later send out-of-bounds.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        existing_packet = %{
          sender: "MOVE",
          base_callsign: "MOVE",
          ssid: "0",
          lat: 33.5,
          lon: -96.0,
          has_position: true,
          received_at: DateTime.utc_now()
        }

        %{
          channel_state
          | socket: %{channel_state.socket | assigns: Map.put(assigns, :visible_packets, %{"MOVE" => existing_packet})}
        }
      end)

      # Now send a batch with the same callsign but moved out-of-bounds — this
      # routes through batch_dispatch({false, true}, ...) and returns
      # {socket, nil, callsign_key}, which feeds remove_markers_if_needed
      # with a non-empty list.
      out_of_bounds_packet = %{
        id: "move-out",
        sender: "MOVE",
        base_callsign: "MOVE",
        ssid: "0",
        lat: 50.0,
        lon: -120.0,
        has_position: true,
        symbol_table_id: "/",
        symbol_code: ">",
        received_at: DateTime.utc_now(),
        path: ""
      }

      send(view.pid, {:packet_batch, [out_of_bounds_packet]})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "set_location with string lat/lng exercises to_float(binary) parse paths", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)

      # Valid binary coords — to_float(binary) → {f, _} → f.
      assert render_hook(view, "set_location", %{"lat" => "33.5", "lng" => "-96.5"})

      # Unparseable binaries — to_float(binary) → :error → nil.
      assert render_hook(view, "set_location", %{"lat" => "not-a-number", "lng" => "also-bad"})

      # Non-binary, non-numeric — to_float(_) → nil fallback.
      assert render_hook(view, "set_location", %{"lat" => %{"junk" => "shape"}, "lng" => :weird})
    end

    test "back-to-back bounds_changed exercises schedule_bounds_update timer-cancel branch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # First bounds_changed schedules a timer.
      render_hook(view, "bounds_changed", %{
        "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0}
      })

      # Second bounds_changed should hit the existing-timer Process.cancel_timer
      # branch (lines 1968+).
      render_hook(view, "bounds_changed", %{
        "bounds" => %{"north" => 35.0, "south" => 31.0, "east" => -94.0, "west" => -98.0}
      })

      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test ":initialize_replay reschedules itself when map_bounds is nil", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Force map_bounds to nil so handle_info_initialize_replay hits the
      # else-branch (line 1042) that schedules :initialize_replay later.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns: assigns |> Map.put(:map_bounds, nil) |> Map.put(:historical_loaded, false)
            }
        }
      end)

      send(view.pid, :initialize_replay)
      Process.sleep(20)
      assert render(view) =~ "aprs-map"
    end

    test "process_bounds_update with out-of-bounds visible_packets exercises remove_markers_batch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Seed visible_packets with packets outside the next-bounds region.
      visible = %{
        "FAR-0" => %{
          sender: "FAR",
          base_callsign: "FAR",
          ssid: "0",
          lat: 60.0,
          lon: -120.0,
          has_position: true,
          received_at: DateTime.utc_now()
        },
        "ALSO-0" => %{
          sender: "ALSO",
          base_callsign: "ALSO",
          ssid: "0",
          lat: 70.0,
          lon: -130.0,
          has_position: true,
          received_at: DateTime.utc_now()
        }
      }

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns: assigns |> Map.put(:visible_packets, visible) |> Map.put(:initial_bounds_loaded, true)
            }
        }
      end)

      # Send bounds that exclude the seeded packets — process_bounds_update
      # should call remove_markers_batch with all of them as marker_ids.
      send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "{:load_rf_path_station_packets, _} with seeded packets builds and adds markers", %{conn: conn} do
      packet_fixture(%{
        sender: "RFST1",
        base_callsign: "RFST1",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("33.0"),
        lon: Decimal.new("-96.0"),
        has_position: true,
        path: ""
      })

      packet_fixture(%{
        sender: "RFST2",
        base_callsign: "RFST2",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("33.5"),
        lon: Decimal.new("-96.5"),
        has_position: true,
        path: ""
      })

      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn)
      send(view.pid, {:load_rf_path_station_packets, ["RFST1", "RFST2"]})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "{:packet_batch, _} at low zoom (untracked) triggers heat map branch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=6", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Force-set map_bounds and zoom directly so the packet_batch processing
      # reaches update_display_for_batch with a non-empty marker_data_list at
      # low zoom (lines 1122-1123 heat map branch).
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:map_bounds, %{north: 40.0, south: 30.0, east: -90.0, west: -100.0})
                  |> Map.put(:map_zoom, 6)
                  |> Map.put(:initial_bounds_loaded, true)
            }
        }
      end)

      packets = [
        %{
          id: "lz-1",
          sender: "LZ-1",
          base_callsign: "LZ",
          ssid: "1",
          lat: 33.0,
          lon: -96.0,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        }
      ]

      send(view.pid, {:packet_batch, packets})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "{:packet_batch, _} at low zoom with tracked callsign triggers trail-line branch", %{conn: conn} do
      packet_fixture(%{
        sender: "LZTR",
        base_callsign: "LZTR",
        ssid: "0",
        received_at: DateTime.utc_now(),
        lat: Decimal.new("33.0"),
        lon: Decimal.new("-96.0"),
        has_position: true,
        path: "WIDE1-1"
      })

      {:ok, view, _html} = live(conn, "/?call=LZTR&lat=33.0&lng=-96.0&z=6", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:map_bounds, %{north: 40.0, south: 30.0, east: -90.0, west: -100.0})
                  |> Map.put(:map_zoom, 6)
                  |> Map.put(:initial_bounds_loaded, true)
                  |> Map.put(:tracked_callsign, "LZTR")
            }
        }
      end)

      packets = [
        %{
          id: "lztr-1",
          sender: "LZTR",
          base_callsign: "LZTR",
          ssid: "0",
          lat: 33.0,
          lon: -96.0,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        }
      ]

      send(view.pid, {:packet_batch, packets})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "{:historical_loading_timeout, generation} matching with historical_loading=true forces completion", %{
      conn: conn
    } do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:historical_loading, true)
                  |> Map.put(:loading_generation, 42)
                  |> Map.put(:total_batches, 5)
            }
        }
      end)

      send(view.pid, {:historical_loading_timeout, 42})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "cleanup_old_packets handler with expired packets in visible_packets prunes them", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn)
      render_hook(view, "map_ready", %{})

      # Seed visible_packets with one fresh and one stale packet.
      now = DateTime.utc_now()
      old = DateTime.add(now, -3600, :second)

      visible = %{
        "FRESH-0" => %{
          sender: "FRESH",
          base_callsign: "FRESH",
          ssid: "0",
          lat: 33.0,
          lon: -96.0,
          has_position: true,
          received_at: now
        },
        "STALE-0" => %{
          sender: "STALE",
          base_callsign: "STALE",
          ssid: "0",
          lat: 33.0,
          lon: -96.0,
          has_position: true,
          received_at: old
        }
      }

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        %{
          channel_state
          | socket: %{
              channel_state.socket
              | assigns:
                  assigns
                  |> Map.put(:visible_packets, visible)
                  |> Map.put(:packet_age_threshold, DateTime.add(now, -1800, :second))
            }
        }
      end)

      send(view.pid, :cleanup_old_packets)
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "tracked-callsign with positionless incoming exercises preferred_tracked_packet branches", %{conn: conn} do
      # Set up a tracked callsign with a current packet that has position.
      # Send incoming packets without position to walk through the cond
      # branches in preferred_tracked_packet/2.
      {:ok, view, _html} = live(conn, "/?call=PREF&lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        # Pre-load tracked_callsign_latest_packet so preferred_tracked_packet/2
        # has a non-nil current_packet to compare against.
        current = %{
          sender: "PREF",
          base_callsign: "PREF",
          ssid: "0",
          lat: 33.0,
          lon: -96.0,
          has_position: true,
          received_at: DateTime.add(DateTime.utc_now(), -120, :second)
        }

        %{
          channel_state
          | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)}
        }
      end)

      # Incoming packet without position — exercises lines 1842, 1843
      # (current has position, incoming doesn't → current returned).
      no_pos_packet = %{
        id: "nopos-1",
        sender: "PREF",
        base_callsign: "PREF",
        ssid: "0",
        lat: nil,
        lon: nil,
        has_position: false,
        received_at: DateTime.utc_now(),
        path: ""
      }

      send(view.pid, {:packet_batch, [no_pos_packet]})
      Process.sleep(50)

      # Both nil-position; newer DateTime returned.
      :sys.replace_state(view.pid, fn channel_state ->
        %{assigns: assigns} = channel_state.socket

        current_no_pos = %{
          sender: "PREF",
          base_callsign: "PREF",
          ssid: "0",
          lat: nil,
          lon: nil,
          has_position: false,
          received_at: DateTime.add(DateTime.utc_now(), -1000, :second)
        }

        %{
          channel_state
          | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current_no_pos)}
        }
      end)

      send(view.pid, {:packet_batch, [no_pos_packet]})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end

    test "tracked-callsign packet_batch processes the matching-sender branch", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/?call=BATCH&lat=33.0&lng=-96.0&z=12", on_error: :warn)
      render_hook(view, "map_ready", %{})

      render_hook(view, "bounds_changed", %{
        "bounds" => %{
          "north" => 34.0,
          "south" => 32.0,
          "east" => -95.0,
          "west" => -97.0
        }
      })

      packets = [
        %{
          id: "btr-1",
          sender: "BATCH",
          base_callsign: "BATCH",
          ssid: "0",
          lat: 33.5,
          lon: -96.0,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        },
        %{
          id: "btr-2",
          sender: "OTHER",
          base_callsign: "OTHER",
          ssid: "1",
          lat: 33.5,
          lon: -96.5,
          has_position: true,
          symbol_table_id: "/",
          symbol_code: ">",
          received_at: DateTime.utc_now(),
          path: ""
        }
      ]

      send(view.pid, {:packet_batch, packets})
      Process.sleep(50)
      assert render(view) =~ "aprs-map"
    end
  end
end
