defmodule Aprsme.ShutdownHandlerTest do
  # async: false — shares app env and the named ShutdownHandler process.
  use ExUnit.Case, async: false

  alias Aprsme.ShutdownHandler

  describe "handle_call(:shutting_down?, _, state)" do
    test "reflects the shutting_down flag in state" do
      # Pure callback invocation — no side effects.
      assert {:reply, false, %{shutting_down: false}} =
               ShutdownHandler.handle_call(:shutting_down?, self(), %{
                 shutting_down: false,
                 drain_timeout: 0
               })

      assert {:reply, true, %{shutting_down: true}} =
               ShutdownHandler.handle_call(:shutting_down?, self(), %{
                 shutting_down: true,
                 drain_timeout: 0
               })
    end
  end

  describe "handle_call(:shutdown, _, state)" do
    test "returns :already_shutting_down when already in progress" do
      state = %{shutting_down: true, drain_timeout: 0}

      assert {:reply, :already_shutting_down, ^state} =
               ShutdownHandler.handle_call(:shutdown, self(), state)
    end
  end

  describe "handle_info({:EXIT, pid, reason}, state)" do
    test "is a no-op that keeps state intact" do
      state = %{shutting_down: false, drain_timeout: 0}

      assert {:noreply, ^state} =
               ShutdownHandler.handle_info({:EXIT, self(), :normal}, state)
    end
  end

  describe "shutting_down?/0 without the GenServer running" do
    test "returns false via the rescue clause" do
      # The supervised ShutdownHandler may or may not be running. If the
      # GenServer.call raises, the `rescue _ -> false` kicks in and we get
      # a boolean back either way.
      assert ShutdownHandler.shutting_down?() == false
    end
  end

  describe "init/1" do
    test "traps exits and reads drain timeout from the environment" do
      # Run in a child process so we don't trip up the test process with a
      # sticky :trap_exit flag.
      task =
        Task.async(fn ->
          prev = System.get_env("DRAIN_TIMEOUT_MS")
          System.put_env("DRAIN_TIMEOUT_MS", "1234")

          try do
            assert {:ok, state} = ShutdownHandler.init([])
            assert state.shutting_down == false
            assert state.drain_timeout == 1234
            assert Process.info(self(), :trap_exit) == {:trap_exit, true}
          after
            if prev do
              System.put_env("DRAIN_TIMEOUT_MS", prev)
            else
              System.delete_env("DRAIN_TIMEOUT_MS")
            end
          end
        end)

      Task.await(task)
    end
  end

  describe "handle_call(:shutdown, _, state) starting shutdown" do
    test "marks state as shutting_down" do
      # initiate_shutdown also schedules :begin_connection_drain after 15s via
      # Process.send_after — too long to wait for in tests. We just verify the
      # state transition; handle_info(:begin_connection_drain) is tested below.
      state = %{shutting_down: false, drain_timeout: 50}

      assert {:reply, :ok, new_state} =
               ShutdownHandler.handle_call(:shutdown, self(), state)

      assert new_state.shutting_down == true
    end
  end

  describe "handle_info(:begin_connection_drain, state)" do
    test "schedules force_shutdown after drain_timeout" do
      state = %{shutting_down: true, drain_timeout: 50}

      assert {:noreply, ^state} =
               ShutdownHandler.handle_info(:begin_connection_drain, state)

      assert_receive :force_shutdown, 1000
    end
  end

  describe "terminate/2" do
    test "returns :ok for normal shutdowns without invoking drain" do
      state = %{shutting_down: false, drain_timeout: 60_000}
      assert :ok = ShutdownHandler.terminate(:shutdown, state)
      assert :ok = ShutdownHandler.terminate({:shutdown, :any}, state)
      assert :ok = ShutdownHandler.terminate(:normal, state)
    end

    test "returns :ok when state is already shutting_down" do
      # Abnormal reason, but shutting_down is already true — no drain sleep.
      state = %{shutting_down: true, drain_timeout: 60_000}
      assert :ok = ShutdownHandler.terminate(:crash, state)
    end
  end

  describe "shutdown/0 as an external API" do
    test "returns the call result if the GenServer is up, else raises" do
      # shutting_down?/0 is safe to call — it rescues when the GenServer isn't running.
      assert ShutdownHandler.shutting_down?() == false
    end
  end

  describe "terminate/2 with abnormal reason" do
    test "runs drain path with a tiny drain_timeout and returns :ok" do
      # Use a near-zero drain_timeout so the Process.sleep inside the graceful
      # drain path is effectively instant. This exercises mark_unhealthy +
      # stop_accepting_connections + Process.sleep without blocking the suite.
      prev_status = Application.get_env(:aprsme, :health_status)
      state = %{shutting_down: false, drain_timeout: 1}

      try do
        assert :ok = ShutdownHandler.terminate(:killed, state)
        # Drain path flips the app's health status to :draining.
        assert Application.get_env(:aprsme, :health_status) == :draining
      after
        if prev_status do
          Application.put_env(:aprsme, :health_status, prev_status)
        else
          Application.delete_env(:aprsme, :health_status)
        end
      end
    end
  end
end
