defmodule Aprsme.Cluster.ConnectionManagerTest do
  use ExUnit.Case, async: false

  alias Aprsme.Cluster.ConnectionManager
  alias Aprsme.Cluster.LeaderElection

  setup do
    # Stop any running ConnectionManager process
    if Process.whereis(ConnectionManager) do
      GenServer.stop(ConnectionManager)
    end

    # Stop any running LeaderElection process
    if Process.whereis(LeaderElection) do
      GenServer.stop(LeaderElection)
    end

    # Ensure DynamicSupervisor is started
    case Process.whereis(Aprsme.DynamicSupervisor) do
      nil ->
        {:ok, _} = DynamicSupervisor.start_link(strategy: :one_for_one, name: Aprsme.DynamicSupervisor)

      pid when is_pid(pid) ->
        :ok
    end

    on_exit(fn ->
      # Try to stop processes but don't crash if they're already dead
      try do
        if Process.whereis(ConnectionManager) do
          GenServer.stop(ConnectionManager, :normal, 100)
        end
      catch
        :exit, _ -> :ok
      end

      try do
        if Process.whereis(LeaderElection) do
          GenServer.stop(LeaderElection, :normal, 100)
        end
      catch
        :exit, _ -> :ok
      end

      # Clean up any IsSupervisor processes if DynamicSupervisor exists
      if Process.whereis(Aprsme.DynamicSupervisor) do
        try do
          children = DynamicSupervisor.which_children(Aprsme.DynamicSupervisor)

          Enum.each(children, fn {_, pid, _, _} ->
            if is_pid(pid) do
              DynamicSupervisor.terminate_child(Aprsme.DynamicSupervisor, pid)
            end
          end)
        catch
          :exit, _ -> :ok
        end
      end
    end)

    :ok
  end

  describe "start_link/1" do
    test "starts the GenServer process" do
      assert {:ok, pid} = ConnectionManager.start_link([])
      assert Process.alive?(pid)
      assert Process.whereis(ConnectionManager) == pid
    end

    test "registers with the module name" do
      {:ok, _pid} = ConnectionManager.start_link([])
      assert Process.whereis(ConnectionManager)
    end
  end

  describe "initialization" do
    setup do
      # Start a minimal LeaderElection to prevent crashes
      Application.put_env(:aprsme, :cluster_enabled, false)
      {:ok, _} = LeaderElection.start_link([])
      LeaderElection.leader?()
      :ok
    end

    test "subscribes to leadership changes" do
      {:ok, pid} = ConnectionManager.start_link([])

      # Verify subscription by publishing a test message
      Phoenix.PubSub.broadcast(
        Aprsme.PubSub,
        "cluster:leadership",
        {:leadership_change, :test_node, false}
      )

      # Sync with the GenServer so the broadcast has been processed.
      :sys.get_state(pid)

      # Process should still be alive after receiving message
      assert Process.whereis(ConnectionManager)
    end

    test "schedules initial state check" do
      {:ok, pid} = ConnectionManager.start_link([])

      # Sync so the initial check (delay=0 in test config) has fired.
      :sys.get_state(pid)

      # Process should still be alive
      assert Process.alive?(pid)
    end
  end

  describe "leadership state handling" do
    setup do
      # Start LeaderElection in non-clustered mode so it becomes leader
      Application.put_env(:aprsme, :cluster_enabled, false)
      {:ok, _} = LeaderElection.start_link([])
      LeaderElection.leader?()

      # Start ConnectionManager
      {:ok, pid} = ConnectionManager.start_link([])
      {:ok, pid: pid}
    end

    test "handles initial state check when leader", %{pid: pid} do
      send(pid, :check_initial_state)
      :sys.get_state(pid)

      # Should handle without crashing
      assert Process.alive?(pid)
      # Verify handle_info handles unknown messages
      assert {:noreply, _} = ConnectionManager.handle_info(:unknown_msg, %{connection_started: false})
    end

    test "handles initial state check when not leader", %{pid: pid} do
      send(pid, :check_initial_state)
      :sys.get_state(pid)

      # Should handle without crashing
      assert Process.alive?(pid)
      # Verify handle_info handles unknown messages
      assert {:noreply, _} = ConnectionManager.handle_info(:unknown_msg, %{connection_started: false})
    end
  end

  describe "leadership change handling" do
    setup do
      # Ensure LeaderElection is started
      Application.put_env(:aprsme, :cluster_enabled, false)
      {:ok, _} = LeaderElection.start_link([])
      LeaderElection.leader?()

      {:ok, pid} = ConnectionManager.start_link([])
      :sys.get_state(pid)
      {:ok, pid: pid}
    end

    test "starts connection when becoming leader", %{pid: pid} do
      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Should handle the message without crashing
      assert Process.alive?(pid)
      # Verify handle_info handles leadership changes directly
      assert {:noreply, _} =
               ConnectionManager.handle_info({:leadership_change, :other@node, true}, %{connection_started: false})
    end

    test "stops connection when losing leadership", %{pid: pid} do
      # First become leader and start connection
      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Then lose leadership
      send(pid, {:leadership_change, node(), false})
      :sys.get_state(pid)

      # Should handle the message without crashing
      assert Process.alive?(pid)
      # Verify handle_info handles losing leadership directly
      assert {:noreply, %{connection_started: false}} =
               ConnectionManager.handle_info({:leadership_change, node(), false}, %{connection_started: true})
    end

    test "ignores leadership changes for other nodes", %{pid: pid} do
      other_node = :other@host

      send(pid, {:leadership_change, other_node, true})
      :sys.get_state(pid)

      # Should not affect this node
      assert Process.alive?(pid)
      # Verify handle_info ignores other nodes directly
      state = %{connection_started: false}
      assert {:noreply, ^state} = ConnectionManager.handle_info({:leadership_change, other_node, true}, state)
    end

    test "does not start connection twice", %{pid: pid} do
      # Become leader twice
      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Should handle duplicate leadership without issues
      assert Process.alive?(pid)
      # Verify handle_info handles duplicate leadership
      assert {:noreply, _} =
               ConnectionManager.handle_info({:leadership_change, node(), true}, %{connection_started: true})
    end
  end

  describe "APRS connection management" do
    setup do
      # Ensure LeaderElection is started
      Application.put_env(:aprsme, :cluster_enabled, false)
      {:ok, _} = LeaderElection.start_link([])
      LeaderElection.leader?()

      {:ok, pid} = ConnectionManager.start_link([])
      :sys.get_state(pid)
      {:ok, pid: pid}
    end

    test "handles IsSupervisor already started error", %{pid: pid} do
      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Should handle gracefully even if IsSupervisor fails to start
      assert Process.alive?(pid)
      # Verify handle_info handles unknown messages
      assert {:noreply, _} = ConnectionManager.handle_info(:unknown_msg, %{connection_started: false})
    end

    test "handles IsSupervisor start failure", %{pid: pid} do
      # Define a failing IsSupervisor
      defmodule FailingIsSupervisor do
        @moduledoc false
        def start_link(_opts) do
          {:error, :intentional_failure}
        end
      end

      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Should handle failure gracefully
      assert Process.alive?(pid)
    end
  end

  describe "handle_leadership_change direct callbacks" do
    test "leader notification for a different node is a no-op" do
      # Send via PubSub message format — should fall through the no-op clause.
      state = %{connection_started: false}

      # handle_info is called on the server; we can't easily test the
      # private helper directly, but we can verify the public PubSub
      # path is idempotent for foreign nodes.
      assert {:noreply, ^state} =
               ConnectionManager.handle_info({:leadership_change, :other@node, true}, state)
    end

    test "gaining leadership when we already have it is a no-op" do
      state = %{connection_started: true}

      # We're the local node but we're already running — should be a no-op
      # path via handle_leadership_change(true, true, %{connection_started: true}).
      # Actually that clause isn't defined — our node being told we won again
      # falls through the other clauses.
      assert {:noreply, _new_state} =
               ConnectionManager.handle_info({:leadership_change, node(), true}, state)
    end

    test "losing leadership when not running is a no-op" do
      state = %{connection_started: false}

      assert {:noreply, ^state} =
               ConnectionManager.handle_info({:leadership_change, node(), false}, state)
    end
  end

  describe "connection lifecycle" do
    setup do
      # Ensure LeaderElection is started
      Application.put_env(:aprsme, :cluster_enabled, false)
      {:ok, _} = LeaderElection.start_link([])
      LeaderElection.leader?()

      {:ok, pid} = ConnectionManager.start_link([])
      {:ok, pid: pid}
    end

    test "full lifecycle - become leader, lose leadership", %{pid: pid} do
      :sys.get_state(pid)

      # Become leader
      send(pid, {:leadership_change, node(), true})
      :sys.get_state(pid)

      # Verify connection started (check state indirectly)
      assert Process.alive?(pid)

      # Lose leadership
      send(pid, {:leadership_change, node(), false})
      :sys.get_state(pid)

      # Verify still alive after stopping connection
      assert Process.alive?(pid)
      # Verify handle_info handles losing leadership directly
      assert {:noreply, %{connection_started: false}} =
               ConnectionManager.handle_info({:leadership_change, node(), false}, %{connection_started: true})
    end
  end

  describe "handle_info direct callbacks" do
    test "handle_info/2 ignores unknown messages without changing state" do
      state = %{connection_started: false}
      assert {:noreply, ^state} = ConnectionManager.handle_info(:totally_unrelated, state)
      assert {:noreply, ^state} = ConnectionManager.handle_info({:foo, :bar}, state)
    end

    test "handle_info/2 :check_initial_state when not leader returns state unchanged" do
      # Ensure LeaderElection is NOT running so leader_check returns false (via :exit catch)
      if Process.whereis(LeaderElection) do
        GenServer.stop(LeaderElection)
      end

      state = %{connection_started: false}

      assert {:noreply, ^state} =
               ConnectionManager.handle_info(:check_initial_state, state)
    end

    test "handle_info/2 :check_initial_state with running leader starts connection" do
      Application.put_env(:aprsme, :cluster_enabled, false)

      case Process.whereis(LeaderElection) do
        nil ->
          {:ok, _} = LeaderElection.start_link([])
          LeaderElection.leader?()

        _pid ->
          :ok
      end

      state = %{connection_started: false}

      # Triggers start_aprs_connection via DynamicSupervisor — may succeed,
      # fail, or report already_started, but must not crash.
      assert {:noreply, new_state} =
               ConnectionManager.handle_info(:check_initial_state, state)

      assert new_state.connection_started == true
    end

    test "handle_info/2 leadership_change — us becoming leader starts connection" do
      state = %{connection_started: false}

      assert {:noreply, new_state} =
               ConnectionManager.handle_info({:leadership_change, node(), true}, state)

      assert new_state.connection_started == true
    end

    test "handle_info/2 leadership_change — us losing leadership stops connection" do
      state = %{connection_started: true}

      assert {:noreply, new_state} =
               ConnectionManager.handle_info({:leadership_change, node(), false}, state)

      assert new_state.connection_started == false
    end
  end

  describe "terminate/2" do
    test "unsubscribes from the cluster:leadership topic and returns :ok" do
      state = %{connection_started: false}
      assert :ok = ConnectionManager.terminate(:normal, state)
    end
  end
end
