defmodule Aprsme.IsTest do
  @moduledoc """
  Tests for Aprsme.Is GenServer — APRS-IS connection management.
  """
  use ExUnit.Case, async: false
  use Aprsme.DataCase

  import ExUnit.CaptureLog

  alias Ecto.Adapters.SQL.Sandbox

  # Helper to build a default GenServer state for direct callback testing
  defp build_state(overrides \\ %{}) do
    timer = Process.send_after(self(), :noop_timer, to_timeout(minute: 5))
    keepalive_timer = Process.send_after(self(), :noop_keepalive, to_timeout(minute: 5))

    base = %{
      server: "mock.aprs.test",
      port: 14_580,
      socket: nil,
      timer: timer,
      keepalive_timer: keepalive_timer,
      connected_at: DateTime.utc_now(),
      packet_stats: %{
        total_packets: 0,
        last_packet_at: nil,
        packets_per_second: 0,
        last_second_count: 0,
        last_second_timestamp: System.system_time(:second)
      },
      buffer: "",
      login_params: %{
        user_id: "TEST",
        passcode: "-1",
        filter: "r/33/-96/100"
      },
      backpressure_active: false,
      safety_valve_timer: nil,
      failure_started_at: nil
    }

    Map.merge(base, overrides)
  end

  # Ensure ETS table exists for tests that need it
  defp ensure_ets_table do
    case :ets.info(:aprsme) do
      :undefined ->
        :ets.new(:aprsme, [:set, :public, :named_table, write_concurrency: true])
        :ets.insert(:aprsme, {:message_number, 0})

      _ ->
        :ok
    end
  end

  describe "init/1" do
    test "stops in test environment" do
      assert {:stop, :test_environment_disabled} = Aprsme.Is.init([])
    end

    test "start_link returns error in test environment" do
      Process.flag(:trap_exit, true)
      result = Aprsme.Is.start_link([])
      assert {:error, :test_environment_disabled} = result
    end
  end

  describe "dispatch/1" do
    test "handles unverified login response" do
      Logger.configure(level: :warning)

      log =
        capture_log(fn ->
          result = Aprsme.Is.dispatch("# logresp TEST unverified, server T2TEXAS")
          assert result == :ok
        end)

      Logger.configure(level: :error)

      assert log =~ "APRS-IS login unverified"
    end

    test "handles accepted login response" do
      Logger.configure(level: :info)

      log =
        capture_log(fn ->
          result = Aprsme.Is.dispatch("# logresp TEST verified, server T2TEXAS")
          assert result == :ok
        end)

      Logger.configure(level: :error)

      assert log =~ "APRS-IS login accepted"
    end

    test "handles comment lines" do
      Logger.configure(level: :debug)

      log =
        capture_log(fn ->
          result = Aprsme.Is.dispatch("# some server comment")
          assert result == :ok
        end)

      Logger.configure(level: :error)

      assert log =~ "COMMENT"
    end

    test "handles empty string" do
      assert Aprsme.Is.dispatch("") == nil
    end

    test "dispatches valid APRS position packet" do
      ensure_ets_table()

      # A real APRS position packet
      raw = "W5ISP-1>APRS,TCPIP*:!3300.00N/09600.00W#PHG2360 Testing"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      # Should not log a parse error
      refute log =~ "PARSE ERROR"
    end

    test "dispatches invalid packet and stores bad packet" do
      Logger.configure(level: :debug)

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch("totally invalid data that cannot be parsed")
        end)

      Logger.configure(level: :warning)

      # Should not crash — the function handles errors gracefully
      assert log =~ "PARSE ERROR"
    end

    test "handles minimal valid APRS packet with empty body" do
      Logger.configure(level: :debug)

      log =
        capture_log(fn ->
          # "X>Y:" is a minimal valid APRS packet (sender X, destination Y, empty body).
          # Aprs.parse/1 returns {:ok, ...} for this input, so no PARSE ERROR is emitted.
          Aprsme.Is.dispatch("X>Y:")
        end)

      Logger.configure(level: :warning)

      refute log =~ "PARSE ERROR"
    end
  end

  describe "handle_call(:get_status, ...)" do
    test "returns status when connected (socket present)" do
      connected_at = DateTime.utc_now()

      state =
        build_state(%{
          socket: :fake_socket,
          connected_at: connected_at
        })

      {:reply, status, ^state} = Aprsme.Is.handle_call(:get_status, {self(), make_ref()}, state)

      assert status.connected == true
      assert status.server == "mock.aprs.test"
      assert status.port == 14_580
      assert status.login_id == "TEST"
      assert status.filter == "r/33/-96/100"
      assert status.connected_at == connected_at
      assert status.uptime_seconds >= 0
      assert status.packet_stats.total_packets == 0
    end

    test "returns disconnected status when socket is nil" do
      state = build_state(%{socket: nil})

      {:reply, status, ^state} = Aprsme.Is.handle_call(:get_status, {self(), make_ref()}, state)

      assert status.connected == false
      assert status.uptime_seconds == 0
      assert status.connected_at == nil
    end

    test "converts charlist server to string in status" do
      state = build_state(%{server: ~c"dallas.aprs2.net"})

      {:reply, status, _state} = Aprsme.Is.handle_call(:get_status, {self(), make_ref()}, state)

      assert status.server == "dallas.aprs2.net"
    end
  end

  describe "handle_call({:send_message, ...}, ...)" do
    test "returns error when not connected (socket nil)" do
      state = build_state(%{socket: nil})

      {:reply, result, ^state} =
        Aprsme.Is.handle_call({:send_message, "test"}, {self(), make_ref()}, state)

      assert result == {:error, :not_connected}
    end
  end

  describe "send_message/1 when GenServer is not running" do
    test "returns not_connected instead of crashing" do
      assert Aprsme.Is.send_message("test message") == {:error, :not_connected}
    end
  end

  describe "send_message/1 when GenServer is running" do
    test "calls into the live process" do
      # Spawn a tiny GenServer registered as Aprsme.Is so send_message/1 hits
      # the `_pid -> GenServer.call(__MODULE__, ...)` branch.
      original = Process.whereis(Aprsme.Is)
      if original, do: :erlang.unregister(Aprsme.Is)

      {:ok, fake_pid} =
        GenServer.start(__MODULE__.FakeIs, :ok)

      Process.register(fake_pid, Aprsme.Is)

      try do
        # The fake responds with :ok to {:send_message, _} calls.
        assert Aprsme.Is.send_message("hello") == :ok
      after
        if Process.whereis(Aprsme.Is) == fake_pid, do: :erlang.unregister(Aprsme.Is)
        if original && !Process.whereis(Aprsme.Is), do: Process.register(original, Aprsme.Is)
        if Process.alive?(fake_pid), do: GenServer.stop(fake_pid)
      end
    end
  end

  defmodule FakeIs do
    @moduledoc false
    use GenServer

    def init(state), do: {:ok, state}
    def handle_call({:send_message, _msg}, _from, state), do: {:reply, :ok, state}
  end

  describe "init/1 disable_connection branch in non-test env" do
    test "init with non-test env + disable_connection=true returns {:stop, :test_environment_disabled}" do
      original_env = Application.get_env(:aprsme, :env)
      original_disable = Application.get_env(:aprsme, :disable_aprs_connection)

      try do
        Application.put_env(:aprsme, :env, :prod)
        Application.put_env(:aprsme, :disable_aprs_connection, true)

        # Hits do_init(:prod, true) — line 60 Logger.warning + {:stop, _}.
        assert {:stop, :test_environment_disabled} = Aprsme.Is.init([])
      after
        Application.put_env(:aprsme, :env, original_env)
        Application.put_env(:aprsme, :disable_aprs_connection, original_disable)
      end
    end
  end

  describe "start_link/1 in test env" do
    test "starts and immediately returns an error due to {:stop, _} from init" do
      # The GenServer link will exit when init returns {:stop, _}, so we run
      # it in a temporarily trap-exit task to avoid taking down the test pid.
      Process.flag(:trap_exit, true)

      try do
        result = Aprsme.Is.start_link([])

        assert match?({:error, _}, result)
      catch
        :exit, _ -> :ok
      after
        Process.flag(:trap_exit, false)
      end
    end
  end

  describe "handle_info(:aprsme_no_message_timeout, ...)" do
    test "ignores timeout when socket is nil" do
      state = build_state(%{socket: nil})

      assert {:noreply, ^state} = Aprsme.Is.handle_info(:aprsme_no_message_timeout, state)
    end

    test "stops when socket is present (timeout detected)" do
      state = build_state(%{socket: :fake_socket})

      log =
        capture_log(fn ->
          assert {:stop, :aprsme_timeout, ^state} =
                   Aprsme.Is.handle_info(:aprsme_no_message_timeout, state)
        end)

      assert log =~ "Socket timeout detected"
    end
  end

  describe "handle_info(:send_keepalive, ...)" do
    test "reschedules keepalive when socket is nil" do
      state = build_state(%{socket: nil})

      assert {:noreply, new_state} = Aprsme.Is.handle_info(:send_keepalive, state)
      assert new_state.socket == nil
      # Timer should be rescheduled even when disconnected
      assert new_state.keepalive_timer != state.keepalive_timer
    end
  end

  describe "handle_info({:tcp, ...}, ...)" do
    test "processes complete lines from TCP data" do
      ensure_ets_table()
      state = build_state()

      # Send a comment line that won't trigger external deps
      data = "# server comment\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)

        # Buffer should be empty after processing complete line
        assert new_state.buffer == ""
        # Timer should be reset
        assert new_state.timer != state.timer
        # Packet stats should be updated
        assert new_state.packet_stats.total_packets == 1
      end)
    end

    test "buffers incomplete lines" do
      state = build_state()

      # Send data without a newline
      data = "# incomplete line"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)

        assert new_state.buffer == "# incomplete line"
      end)
    end

    test "handles multi-line data" do
      state = build_state()

      data = "# line1\r\n# line2\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)

        assert new_state.buffer == ""
      end)
    end

    test "handles split lines across multiple TCP messages" do
      state = build_state()

      # First chunk with incomplete line
      data1 = "# partial"

      capture_log(fn ->
        {:noreply, state2} = Aprsme.Is.handle_info({:tcp, :fake_port, data1}, state)
        assert state2.buffer == "# partial"

        # Second chunk completes the line
        data2 = " line\r\n"
        {:noreply, state3} = Aprsme.Is.handle_info({:tcp, :fake_port, data2}, state2)
        assert state3.buffer == ""
      end)
    end

    test "skips empty lines" do
      state = build_state()

      data = "\r\n\r\n# real line\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)
        assert new_state.buffer == ""
      end)
    end

    test "updates packet stats with per-second counter" do
      state = build_state()

      data = "# line\r\n"

      capture_log(fn ->
        {:noreply, state2} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)
        assert state2.packet_stats.total_packets == 1
        assert state2.packet_stats.last_packet_at

        # Send another in the same second
        {:noreply, state3} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state2)
        assert state3.packet_stats.total_packets == 2
      end)
    end
  end

  # SSL handler was removed — connection uses :gen_tcp, not SSL (TODO #25)

  describe "handle_info({:tcp_closed, ...}, ...)" do
    test "schedules reconnect and clears socket" do
      state = build_state(%{socket: :fake_socket})

      {:noreply, new_state} = Aprsme.Is.handle_info({:tcp_closed, :fake_socket}, state)

      assert new_state.socket == nil
      assert new_state.timer == nil
      assert new_state.keepalive_timer == nil
    end
  end

  describe "handle_info({:tcp_error, ...}, ...)" do
    test "schedules reconnect on error" do
      state = build_state(%{socket: :fake_socket})

      {:noreply, new_state} =
        Aprsme.Is.handle_info({:tcp_error, :fake_socket, :econnrefused}, state)

      assert new_state.socket == nil
      assert new_state.timer == nil
      assert new_state.keepalive_timer == nil
    end
  end

  describe "handle_info(:reconnect, ...)" do
    test "reconnect attempt is blocked in test environment" do
      state = build_state()

      {:noreply, new_state} = Aprsme.Is.handle_info(:reconnect, state)

      # Socket should remain nil since connection is blocked in test env
      assert new_state.socket == nil
    end
  end

  describe "dispatch/1 with various APRS formats" do
    setup do
      ensure_ets_table()
      Logger.configure(level: :error)
      on_exit(fn -> Logger.configure(level: :error) end)
      :ok
    end

    test "dispatches a position-with-timestamp packet" do
      raw = "N0CALL>APRS,WIDE1-1:/092345z3300.00N/09600.00W>Test"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches a telemetry packet" do
      raw = "N0CALL>APRS,WIDE1-1:T#001,000,000,000,000,000,00000000"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches a status packet" do
      raw = "N0CALL>APRS,WIDE1-1:>Status update text"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches a message packet" do
      raw = "N0CALL>APRS,WIDE1-1::KE5ABC   :Hello{001"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches an object packet" do
      raw = "N0CALL>APRS,WIDE1-1:;MyObj    *111111z3300.00N/09600.00W-"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches an item packet" do
      raw = "N0CALL>APRS,WIDE1-1:)MyItem!3300.00N/09600.00W-"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches an empty string (no-op)" do
      assert Aprsme.Is.dispatch("") == nil
    end

    test "dispatches a comment (starts with #)" do
      capture_log(fn ->
        # dispatch returns :ok for server comments without crashing.
        assert Aprsme.Is.dispatch("# server comment") in [nil, :ok]
      end)
    end
  end

  describe "handle_info({:tcp, socket, data}, state) buffer edge cases" do
    test "routes a single newline-terminated line through dispatch" do
      ensure_ets_table()
      state = build_state()

      raw_line = "N0CALL>APRS,WIDE1-1:>Hello\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_socket, raw_line}, state)
        assert new_state.buffer == ""
      end)
    end

    test "keeps incomplete trailing data in buffer" do
      ensure_ets_table()
      state = build_state()

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_socket, "partial"}, state)
        assert new_state.buffer == "partial"
      end)
    end

    test "concatenates previous buffer + new data for a complete line" do
      ensure_ets_table()
      state = build_state(%{buffer: "prefix"})

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_socket, "suffix\n"}, state)
        assert new_state.buffer == ""
      end)
    end
  end

  describe "terminate/2" do
    test "terminates cleanly with nil socket" do
      state = build_state(%{socket: nil})

      result = Aprsme.Is.terminate(:normal, state)
      assert result == :normal
    end

    test "terminates cleanly with incomplete buffer data" do
      state = build_state(%{socket: nil, buffer: "incomplete data"})

      result = Aprsme.Is.terminate(:normal, state)
      assert result == :normal
    end

    test "terminates cleanly with empty buffer" do
      state = build_state(%{socket: nil, buffer: ""})

      result = Aprsme.Is.terminate(:normal, state)
      assert result == :normal
    end

    test "handles state without buffer key" do
      # terminate uses Map.get with default, so missing :buffer should be fine
      state = Map.delete(build_state(), :buffer)

      capture_log(fn ->
        assert Aprsme.Is.terminate(:shutdown, state) == :normal
      end)
    end

    test "cancels timers on terminate" do
      timer = Process.send_after(self(), :timer_test, to_timeout(minute: 5))
      keepalive = Process.send_after(self(), :keepalive_test, to_timeout(minute: 5))

      state = build_state(%{socket: nil, timer: timer, keepalive_timer: keepalive})

      capture_log(fn ->
        Aprsme.Is.terminate(:normal, state)
      end)

      # Timers should have been cancelled
      assert Process.cancel_timer(timer) == false
      assert Process.cancel_timer(keepalive) == false
    end

    test "closes an active socket on terminate" do
      {client, listen, cleanup} = loopback_socket()
      state = build_state(%{socket: client, buffer: ""})

      capture_log(fn ->
        assert Aprsme.Is.terminate(:normal, state) == :normal
      end)

      # Socket should be closed after terminate
      assert :gen_tcp.send(client, "X") == {:error, :closed}

      :gen_tcp.close(listen)
      cleanup.()
    end

    test "cancels safety_valve_timer on terminate" do
      safety_timer = Process.send_after(self(), :safety, to_timeout(minute: 5))
      state = build_state(%{socket: nil, safety_valve_timer: safety_timer})

      capture_log(fn ->
        assert Aprsme.Is.terminate(:normal, state) == :normal
      end)

      assert Process.cancel_timer(safety_timer) == false
    end
  end

  describe "code_change/3" do
    test "returns state unchanged" do
      state = build_state()
      assert {:ok, ^state} = Aprsme.Is.code_change("1.0.0", state, [])
    end
  end

  # Local loopback TCP socket that exercises the active-socket branches.
  # The server side just accepts and discards; the client's send/setopts calls
  # still go through the real kernel. Returns {listen_sock, client_sock}.
  defp loopback_socket do
    {:ok, listen} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
    {:ok, port} = :inet.port(listen)

    # Accept on a spawned process so connect doesn't block.
    test_pid = self()

    spawn_link(fn ->
      case :gen_tcp.accept(listen, 5_000) do
        {:ok, server_sock} -> send(test_pid, {:server_sock, server_sock})
        _ -> :ok
      end
    end)

    {:ok, client} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false])

    receive do
      {:server_sock, _s} -> :ok
    after
      1_000 -> :ok
    end

    on_exit = fn ->
      _ = :gen_tcp.close(client)
      _ = :gen_tcp.close(listen)
    end

    {client, listen, on_exit}
  end

  describe "handle_call({:send_message, _}, _, state) with active socket" do
    setup do
      ensure_ets_table()
      {client, _listen, cleanup} = loopback_socket()
      on_exit(cleanup)
      {:ok, socket: client}
    end

    test "sends a message and returns :ok", %{socket: socket} do
      state = build_state(%{socket: socket})

      capture_log(fn ->
        assert {:reply, :ok, ^state} =
                 Aprsme.Is.handle_call({:send_message, "hello"}, self(), state)
      end)
    end

    test "handles send error by closing socket and scheduling reconnect", %{socket: socket} do
      # Close the socket before send; the send call will return an error.
      :gen_tcp.close(socket)
      state = build_state(%{socket: socket})

      capture_log(fn ->
        assert {:reply, {:error, _reason}, new_state} =
                 Aprsme.Is.handle_call({:send_message, "fail"}, self(), state)

        assert is_nil(new_state.socket)
        assert is_nil(new_state.timer)
        assert is_nil(new_state.keepalive_timer)
      end)
    end
  end

  describe "handle_info(:send_keepalive) with active socket" do
    setup do
      {client, _listen, cleanup} = loopback_socket()
      on_exit(cleanup)
      {:ok, socket: client}
    end

    test "sends keepalive and schedules next", %{socket: socket} do
      state = build_state(%{socket: socket, keepalive_timer: nil})

      capture_log(fn ->
        assert {:noreply, new_state} = Aprsme.Is.handle_info(:send_keepalive, state)
        assert is_reference(new_state.keepalive_timer)
        Process.cancel_timer(new_state.keepalive_timer)
      end)
    end

    test "stops on send error", %{socket: socket} do
      :gen_tcp.close(socket)
      state = build_state(%{socket: socket})

      capture_log(fn ->
        assert {:stop, :normal, _state} = Aprsme.Is.handle_info(:send_keepalive, state)
      end)
    end
  end

  describe "handle_info(:reconnect) with mocked connection" do
    # Start a listener, temporarily swap APRS-IS config to point at it,
    # then trigger :reconnect so Aprsme.Is opens a real TCP connection.
    setup do
      {:ok, listen} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
      {:ok, port} = :inet.port(listen)

      # Spawn an acceptor that reads the login string and closes.
      spawn_link(fn ->
        case :gen_tcp.accept(listen, 5_000) do
          {:ok, srv} ->
            _ = :gen_tcp.recv(srv, 0, 1_000)
            _ = :gen_tcp.close(srv)

          _ ->
            :ok
        end
      end)

      original_disabled = Application.get_env(:aprsme, :disable_aprs_connection, false)
      original_env = Application.get_env(:aprsme, :env)

      Application.put_env(:aprsme, :disable_aprs_connection, false)
      Application.put_env(:aprsme, :env, :dev)

      on_exit(fn ->
        Application.put_env(:aprsme, :disable_aprs_connection, original_disabled)
        Application.put_env(:aprsme, :env, original_env)
        _ = :gen_tcp.close(listen)
      end)

      {:ok, port: port}
    end

    test "reconnect succeeds and updates socket/timers", %{port: port} do
      state = build_state(%{server: ~c"127.0.0.1", port: port, socket: nil, timer: nil, keepalive_timer: nil})

      capture_log(fn ->
        assert {:noreply, new_state} = Aprsme.Is.handle_info(:reconnect, state)

        assert new_state.socket
        assert is_reference(new_state.timer)
        assert is_reference(new_state.keepalive_timer)
        refute new_state.backpressure_active

        # Cleanup
        :gen_tcp.close(new_state.socket)
        if new_state.timer, do: Process.cancel_timer(new_state.timer)
        if new_state.keepalive_timer, do: Process.cancel_timer(new_state.keepalive_timer)
      end)
    end
  end

  describe "init/1 with real TCP connection (not :test env)" do
    test "connects and sets up timers on successful login" do
      {:ok, listen} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
      {:ok, port} = :inet.port(listen)

      spawn_link(fn ->
        case :gen_tcp.accept(listen, 5_000) do
          {:ok, srv} ->
            _ = :gen_tcp.recv(srv, 0, 1_000)
            _ = :gen_tcp.close(srv)

          _ ->
            :ok
        end
      end)

      original_disabled = Application.get_env(:aprsme, :disable_aprs_connection, false)
      original_env = Application.get_env(:aprsme, :env)
      original_server = Application.get_env(:aprsme, :aprs_is_server)
      original_port = Application.get_env(:aprsme, :aprs_is_port)

      Application.put_env(:aprsme, :disable_aprs_connection, false)
      Application.put_env(:aprsme, :env, :dev)
      Application.put_env(:aprsme, :aprs_is_server, ~c"127.0.0.1")
      Application.put_env(:aprsme, :aprs_is_port, port)

      try do
        capture_log(fn ->
          assert {:ok, state} = Aprsme.Is.init([])
          # Socket should be set after successful connect.
          assert is_port(state.socket)
          assert is_reference(state.timer)
          assert is_reference(state.keepalive_timer)

          # Cleanup
          if state.socket, do: :gen_tcp.close(state.socket)
          if state.timer, do: Process.cancel_timer(state.timer)
          if state.keepalive_timer, do: Process.cancel_timer(state.keepalive_timer)
        end)
      after
        Application.put_env(:aprsme, :disable_aprs_connection, original_disabled)
        Application.put_env(:aprsme, :env, original_env)
        if original_server, do: Application.put_env(:aprsme, :aprs_is_server, original_server)
        if original_port, do: Application.put_env(:aprsme, :aprs_is_port, original_port)
        _ = :gen_tcp.close(listen)
      end
    end

    test "handles connect failure gracefully" do
      original_disabled = Application.get_env(:aprsme, :disable_aprs_connection, false)
      original_env = Application.get_env(:aprsme, :env)
      original_server = Application.get_env(:aprsme, :aprs_is_server)
      original_port = Application.get_env(:aprsme, :aprs_is_port)

      Application.put_env(:aprsme, :disable_aprs_connection, false)
      Application.put_env(:aprsme, :env, :dev)
      Application.put_env(:aprsme, :aprs_is_server, ~c"127.0.0.1")
      # Port 1 → connection refused.
      Application.put_env(:aprsme, :aprs_is_port, 1)

      try do
        capture_log(fn ->
          assert {:ok, state} = Aprsme.Is.init([])
          # Connection failed — socket stays nil but state is still returned.
          assert is_nil(state.socket)
        end)
      after
        Application.put_env(:aprsme, :disable_aprs_connection, original_disabled)
        Application.put_env(:aprsme, :env, original_env)
        if original_server, do: Application.put_env(:aprsme, :aprs_is_server, original_server)
        if original_port, do: Application.put_env(:aprsme, :aprs_is_port, original_port)
      end
    end
  end

  describe "handle_info(:reconnect) when connect fails" do
    test "schedules another reconnect attempt on connection error" do
      # Port 1 should always refuse or fail — triggers the connect-error branch.
      original_disabled = Application.get_env(:aprsme, :disable_aprs_connection, false)
      original_env = Application.get_env(:aprsme, :env)

      Application.put_env(:aprsme, :disable_aprs_connection, false)
      Application.put_env(:aprsme, :env, :dev)

      on_exit(fn ->
        Application.put_env(:aprsme, :disable_aprs_connection, original_disabled)
        Application.put_env(:aprsme, :env, original_env)
      end)

      state = build_state(%{server: ~c"127.0.0.1", port: 1, socket: nil, timer: nil, keepalive_timer: nil})

      capture_log(fn ->
        assert {:noreply, same_state} = Aprsme.Is.handle_info(:reconnect, state)
        assert same_state.failure_started_at
      end)
    end
  end

  describe "handle_info({:backpressure, _}) with active socket" do
    setup do
      {client, _listen, cleanup} = loopback_socket()
      on_exit(cleanup)
      {:ok, socket: client}
    end

    test "activate sets backpressure and installs safety valve", %{socket: socket} do
      state = build_state(%{socket: socket, backpressure_active: false})

      capture_log(fn ->
        assert {:noreply, new_state} =
                 Aprsme.Is.handle_info({:backpressure, :activate}, state)

        assert new_state.backpressure_active
        assert is_reference(new_state.safety_valve_timer)
        Process.cancel_timer(new_state.safety_valve_timer)
      end)
    end

    test "deactivate clears backpressure and restores timer", %{socket: socket} do
      state = build_state(%{socket: socket, backpressure_active: true, safety_valve_timer: nil})

      capture_log(fn ->
        assert {:noreply, new_state} =
                 Aprsme.Is.handle_info({:backpressure, :deactivate}, state)

        refute new_state.backpressure_active
        assert is_reference(new_state.timer)
        Process.cancel_timer(new_state.timer)
      end)
    end

    test "safety_valve forces resume when socket is active", %{socket: socket} do
      state = build_state(%{socket: socket, backpressure_active: true, safety_valve_timer: nil})

      capture_log(fn ->
        assert {:noreply, new_state} =
                 Aprsme.Is.handle_info(:backpressure_safety_valve, state)

        refute new_state.backpressure_active
        assert is_reference(new_state.timer)
        Process.cancel_timer(new_state.timer)
      end)
    end

    test "activate is a no-op when backpressure is already active", %{socket: socket} do
      state = build_state(%{socket: socket, backpressure_active: true})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :activate}, state)
    end

    test "activate is a no-op when there is no socket" do
      state = build_state(%{socket: nil, backpressure_active: false})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :activate}, state)
    end

    test "deactivate is a no-op when backpressure is already inactive" do
      state = build_state(%{socket: nil, backpressure_active: false})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :deactivate}, state)
    end
  end

  describe "send_message/3 and /1 helpers" do
    test "three-arg send_message delegates to send_message/1 without server running" do
      # GenServer not running → send_message/1 returns {:error, :not_connected}.
      assert {:error, :not_connected} = Aprsme.Is.send_message("FROM", "TO", "hi")
    end

    test "set_filter/1 without server running returns not_connected" do
      assert {:error, :not_connected} = Aprsme.Is.set_filter("r/0/0/1")
    end

    test "list_active_filters/0 without server running returns not_connected" do
      assert {:error, :not_connected} = Aprsme.Is.list_active_filters()
    end
  end

  describe "backpressure handling" do
    test "activate with nil socket is a no-op" do
      state = build_state(%{socket: nil, backpressure_active: false})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :activate}, state)
    end

    test "activate when already active is a no-op" do
      state = build_state(%{socket: nil, backpressure_active: true})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :activate}, state)
    end

    test "deactivate when already inactive is a no-op" do
      state = build_state(%{socket: nil, backpressure_active: false})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info({:backpressure, :deactivate}, state)
    end

    test "deactivate with nil socket clears flag but doesn't touch socket" do
      state = build_state(%{socket: nil, backpressure_active: true})

      assert {:noreply, new_state} =
               Aprsme.Is.handle_info({:backpressure, :deactivate}, state)

      refute new_state.backpressure_active
      assert is_nil(new_state.safety_valve_timer)
    end

    test "safety_valve when not active is a no-op" do
      state = build_state(%{backpressure_active: false})

      assert {:noreply, ^state} =
               Aprsme.Is.handle_info(:backpressure_safety_valve, state)
    end

    test "safety_valve with nil socket clears flag" do
      state = build_state(%{socket: nil, backpressure_active: true, safety_valve_timer: nil})

      assert {:noreply, new_state} =
               Aprsme.Is.handle_info(:backpressure_safety_valve, state)

      refute new_state.backpressure_active
      assert is_nil(new_state.safety_valve_timer)
    end
  end

  describe "APRS-IS mock functionality" do
    setup do
      case GenServer.start_link(AprsIsMock, [], name: AprsIsMock) do
        {:ok, pid} ->
          on_exit(fn ->
            if Process.alive?(pid), do: GenServer.stop(pid, :normal)
          end)

          {:ok, mock_pid: pid}

        {:error, {:already_started, pid}} ->
          on_exit(fn ->
            if Process.alive?(pid), do: GenServer.stop(pid, :normal)
          end)

          {:ok, mock_pid: pid}
      end
    end

    test "mock should provide status without external connections", %{mock_pid: _pid} do
      status = AprsIsMock.get_status()

      assert status.connected == false
      assert status.server == "mock.aprs.test"
      assert status.port == 14_580
      assert status.login_id == "TEST"
      assert status.packet_stats.total_packets == 0
    end

    test "mock should handle message sending safely", %{mock_pid: _pid} do
      assert AprsIsMock.send_message("test message") == :ok
      assert AprsIsMock.send_message("TEST", "DEST", "hello") == :ok
      assert AprsIsMock.set_filter("r/0/0/1") == :ok
      assert AprsIsMock.list_active_filters() == :ok
    end

    test "mock can simulate packet reception for testing", %{mock_pid: _pid} do
      test_packet = %{
        sender: "TEST-1",
        destination: "APRS",
        path: ["WIDE1-1", "WIDE2-1"],
        data_type: :position,
        latitude: 33.0,
        longitude: -96.0
      }

      assert AprsIsMock.simulate_packet(test_packet) == :ok
    end

    test "mock can simulate connection state changes", %{mock_pid: _pid} do
      assert AprsIsMock.simulate_connection_state(true) == :ok
      status = AprsIsMock.get_status()
      assert status.connected == true
      assert status.connected_at

      assert AprsIsMock.simulate_connection_state(false) == :ok
      status = AprsIsMock.get_status()
      assert status.connected == false
      assert status.connected_at == nil
    end
  end

  describe "network isolation verification" do
    test "no external network calls should be made during test runs" do
      forbidden_servers = [
        "rotate.aprs2.net",
        "dallas.aprs2.net",
        "seattle.aprs2.net",
        "chicago.aprs2.net",
        "atlanta.aprs2.net"
      ]

      current_server = Application.get_env(:aprsme, :aprsme_is_server)

      if current_server do
        server_str = to_string(current_server)

        refute Enum.any?(forbidden_servers, fn forbidden ->
                 String.contains?(server_str, forbidden)
               end),
               "Test environment should not be configured with real APRS servers"
      end

      # Verify Aprsme.Is returns disconnected status without network calls
      status = Aprsme.Is.get_status()
      assert status.connected == false
    end

    test "test environment and connection config are properly set" do
      assert Mix.env() == :test
      assert Application.get_env(:aprsme, :env) == :test
      assert Application.get_env(:aprsme, :disable_aprs_connection) == true

      # Verify Aprsme.Is respects the test-only configuration
      assert Aprsme.Is.init([]) == {:stop, :test_environment_disabled}
    end
  end

  describe "get_status/0 when GenServer is not running" do
    test "returns disconnected status" do
      # Aprsme.Is is not started in test env, so get_status should handle nil process
      status = Aprsme.Is.get_status()

      assert status.connected == false
      assert status.uptime_seconds == 0
      assert status.connected_at == nil
      assert status.packet_stats.total_packets == 0
    end

    test "includes configured server info" do
      status = Aprsme.Is.get_status()

      assert status.port == 14_580
      assert status.login_id == "TEST"
      assert status.filter == "r/33/-96/100"
    end

    test "returns fallback packet storage stats when called from a non-owner process" do
      parent = self()

      owner =
        spawn(fn ->
          :ok = Sandbox.checkout(Aprsme.Repo)
          send(parent, :sandbox_owner_ready)

          receive do
            :stop -> :ok
          end
        end)

      assert_receive :sandbox_owner_ready

      task =
        Task.async(fn ->
          receive do
            :query_status -> Aprsme.Is.get_status()
          end
        end)

      Sandbox.allow(Aprsme.Repo, owner, task.pid)
      send(owner, :stop)
      send(task.pid, :query_status)
      status = Task.await(task)

      assert status.connected == false
      assert status.stored_packet_count == 0
      assert status.oldest_packet_timestamp == nil
    end
  end

  describe "buffer line extraction via TCP data" do
    test "handles \\n line endings" do
      state = build_state()
      data = "# line1\n# line2\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)
        assert new_state.buffer == ""
      end)
    end

    test "handles mixed \\r\\n and \\n line endings" do
      state = build_state()
      data = "# line1\r\n# line2\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)
        assert new_state.buffer == ""
      end)
    end

    test "preserves partial line in buffer across chunks" do
      state = build_state()

      capture_log(fn ->
        {:noreply, s1} = Aprsme.Is.handle_info({:tcp, :fake_port, "# complete\r\npartial"}, state)
        assert s1.buffer == "partial"

        {:noreply, s2} = Aprsme.Is.handle_info({:tcp, :fake_port, " data\r\n"}, s1)
        assert s2.buffer == ""
      end)
    end
  end

  describe "packet stats update via TCP data" do
    test "increments total_packets per data message" do
      state = build_state()

      capture_log(fn ->
        {:noreply, s1} = Aprsme.Is.handle_info({:tcp, :fake_port, "# a\r\n"}, state)
        assert s1.packet_stats.total_packets == 1

        {:noreply, s2} = Aprsme.Is.handle_info({:tcp, :fake_port, "# b\r\n"}, s1)
        assert s2.packet_stats.total_packets == 2

        {:noreply, s3} = Aprsme.Is.handle_info({:tcp, :fake_port, "# c\r\n"}, s2)
        assert s3.packet_stats.total_packets == 3
      end)
    end

    test "records last_packet_at timestamp" do
      state = build_state()

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, "# x\r\n"}, state)
        assert %DateTime{} = new_state.packet_stats.last_packet_at
      end)
    end

    test "resets per-second counter when time advances" do
      # Build a state with last_second_timestamp in the past
      old_stats = %{
        total_packets: 5,
        last_packet_at: DateTime.utc_now(),
        packets_per_second: 3,
        last_second_count: 3,
        last_second_timestamp: System.system_time(:second) - 2
      }

      state = build_state(%{packet_stats: old_stats})

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, "# data\r\n"}, state)

        # Should have reset the per-second counter
        assert new_state.packet_stats.total_packets == 6
        assert new_state.packet_stats.packets_per_second == 1
        assert new_state.packet_stats.last_second_count == 1
      end)
    end

    test "accumulates per-second counter within same second" do
      state = build_state()

      capture_log(fn ->
        {:noreply, s1} = Aprsme.Is.handle_info({:tcp, :fake_port, "# a\r\n"}, state)

        # If within the same second, count should accumulate
        # (This depends on test execution speed, but the logic is correct)
        {:noreply, s2} = Aprsme.Is.handle_info({:tcp, :fake_port, "# b\r\n"}, s1)
        assert s2.packet_stats.total_packets == 2
      end)
    end
  end

  describe "dispatch/1 with APRS packets" do
    setup do
      ensure_ets_table()
      :ok
    end

    test "dispatches a valid position packet without crashing" do
      raw = "N0CALL>APRS,TCPIP*:!3300.00N/09600.00W#Test station"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "dispatches a weather packet" do
      raw = "N0CALL>APRS,TCPIP*:@092345z3300.00N/09600.00W_090/000g005t077"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "handles rescue in dispatch when packet processing raises" do
      Logger.configure(level: :debug)

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch("???")
        end)

      Logger.configure(level: :warning)

      assert log =~ "PARSE ERROR"
    end

    test "stores bad packet on parse error" do
      Logger.configure(level: :debug)

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch("not a valid aprs packet at all")
        end)

      Logger.configure(level: :warning)

      assert log =~ "PARSE ERROR"
    end

    test "dispatches a status packet" do
      raw = "N0CALL>APRS,TCPIP*:>Test status message"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end

    test "handles comment with only whitespace after hash" do
      result = Aprsme.Is.dispatch("#   ")
      assert result == :ok
    end
  end

  describe "handle_info({:tcp_closed, ...}) with nil timers" do
    test "handles tcp_closed when timers are already nil" do
      state = build_state(%{socket: :fake_socket, timer: nil, keepalive_timer: nil})

      {:noreply, new_state} = Aprsme.Is.handle_info({:tcp_closed, :fake_socket}, state)

      assert new_state.socket == nil
      assert new_state.timer == nil
      assert new_state.keepalive_timer == nil
    end
  end

  describe "handle_info({:tcp_error, ...}) with nil timers" do
    test "handles tcp_error when timers are already nil" do
      state = build_state(%{socket: :fake_socket, timer: nil, keepalive_timer: nil})

      {:noreply, new_state} =
        Aprsme.Is.handle_info({:tcp_error, :fake_socket, :econnreset}, state)

      assert new_state.socket == nil
    end
  end

  describe "handle_call(:get_status, ...) server_to_string edge cases" do
    test "handles non-string non-charlist server via to_string fallback" do
      state = build_state(%{server: :some_atom_server})

      {:reply, status, _state} = Aprsme.Is.handle_call(:get_status, {self(), make_ref()}, state)

      assert status.server == "some_atom_server"
    end
  end

  describe "dispatch/1 processes TCP data through full pipeline" do
    setup do
      ensure_ets_table()
      :ok
    end

    test "valid packet is processed through struct_to_map and submitted to pipeline" do
      # A packet with an SSID and path that exercises full dispatch path
      raw = "W5ISP-9>APRS,TCPIP*,qAR,W5ISP:!3300.00N/09600.00W>PHG2360 Mobile"

      log =
        capture_log(fn ->
          Aprsme.Is.dispatch(raw)
        end)

      refute log =~ "PARSE ERROR"
    end
  end

  describe "TCP data with real APRS packets" do
    setup do
      ensure_ets_table()
      :ok
    end

    test "processes a complete APRS packet arriving via TCP" do
      state = build_state()
      raw = "W5ISP-1>APRS,TCPIP*:!3300.00N/09600.00W#Test\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, raw}, state)

        assert new_state.buffer == ""
        assert new_state.packet_stats.total_packets == 1
      end)
    end

    test "processes multiple APRS packets in single TCP message" do
      state = build_state()

      data =
        "# server comment\r\nW5ISP-1>APRS,TCPIP*:!3300.00N/09600.00W#Test\r\n# another comment\r\n"

      capture_log(fn ->
        {:noreply, new_state} = Aprsme.Is.handle_info({:tcp, :fake_port, data}, state)

        assert new_state.buffer == ""
      end)
    end
  end

  describe "get_status/0 with a running process under the module name" do
    test "falls back to the disconnected status when the GenServer doesn't respond" do
      # Register a minimal process under the Aprsme.Is name that doesn't know
      # how to handle :get_status. GenServer.call/3 to a non-GenServer raises
      # :exit — status_from/1 catches it and returns disconnected_status with
      # the rotate.aprs2.net fallback.
      parent = self()

      pid =
        spawn(fn ->
          receive do
            :stop -> :ok
          after
            200 -> :ok
          end

          send(parent, :done)
        end)

      # Unregister any prior registration first.
      if Process.whereis(Aprsme.Is) do
        :erlang.unregister(Aprsme.Is)
      end

      true = Process.register(pid, Aprsme.Is)

      try do
        # Trap exits so the :exit from GenServer.call doesn't kill us.
        Process.flag(:trap_exit, true)

        # capture_log because disconnected_status logs nothing, but be defensive.
        status = Aprsme.Is.get_status()

        assert status.connected == false
        # disconnected_status/1 reads the configured :aprs_is_server, overriding
        # the fallback argument — test config sets it to "mock.aprs.test".
        assert status.server == "mock.aprs.test"
      after
        if Process.whereis(Aprsme.Is) == pid do
          :erlang.unregister(Aprsme.Is)
        end

        send(pid, :stop)
      end
    end
  end
end
