"""Tests for the _spawn_gateway_restart repeat-request cooldown (#89034).

A finished ``gateway-restart`` child does not mean the gateway is back, so the
pre-existing "reuse the in-flight child" guard stops coalescing exactly when
repeat requests are most harmful.  A stale cached dashboard frontend re-firing
its restart every few seconds therefore produced a fresh restart every time
(the reporter measured 77, 17 of them inside one minute), killing the gateway
often enough mid-FTS5-write to corrupt ``state.db``.
"""
from __future__ import annotations

import subprocess
from unittest.mock import MagicMock, patch

import pytest
import hermes_cli.web_server_gateway as _web_server_gateway


@pytest.fixture(autouse=True)
def reset_restart_cooldown():
    """Keep the module-level cooldown state out of neighbouring tests."""
    import hermes_cli.web_server as web_server

    web_server._LAST_GATEWAY_RESTART = None
    yield
    web_server._LAST_GATEWAY_RESTART = None


def _exited_proc(pid: int = 4242) -> MagicMock:
    """A Popen handle for a child that has already finished."""
    proc = MagicMock(spec=subprocess.Popen)
    proc.poll.return_value = 0
    proc.pid = pid
    return proc


class TestRepeatRestartWithinCooldown:
    """Repeats within the window ride the previous spawn."""

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    @patch("hermes_cli.web_server_gateway._ACTION_PROCS", {})
    def test_second_request_after_the_child_exits_is_coalesced(
        self, mock_spawn, mock_subcmd
    ):
        """The exact #89034 shape: child gone, gateway still coming up."""
        from hermes_cli.web_server import _spawn_gateway_restart

        proc = _exited_proc()
        mock_spawn.return_value = proc

        with patch("hermes_cli.gateway._reap_unsupervised_gateway_orphans"), patch(
            "hermes_cli.web_server.time.monotonic", side_effect=[100.0, 103.5]
        ):
            first, first_reused = _spawn_gateway_restart()
            second, second_reused = _spawn_gateway_restart()

        assert mock_spawn.call_count == 1, (
            "a repeat request 3.5s later must not start a second restart"
        )
        assert first is proc and first_reused is False
        assert second is proc and second_reused is True

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    @patch("hermes_cli.web_server_gateway._ACTION_PROCS", {})
    def test_a_storm_of_requests_produces_exactly_one_restart(
        self, mock_spawn, mock_subcmd
    ):
        """17 requests inside one minute, the reported burst rate."""
        from hermes_cli.web_server import _spawn_gateway_restart

        mock_spawn.return_value = _exited_proc()
        # One read to stamp the spawn, then one per coalesced repeat.  The
        # window is anchored to the SPAWN, not to the previous request:
        # anchoring it to the previous request would let a 3.5s-spaced storm
        # walk the deadline forward forever and never restart at all.
        clock = [100.0, 103.5, 107.0]

        with patch("hermes_cli.gateway._reap_unsupervised_gateway_orphans"), patch(
            "hermes_cli.web_server.time.monotonic", side_effect=clock
        ):
            _spawn_gateway_restart()
            _spawn_gateway_restart()
            _spawn_gateway_restart()

        assert mock_spawn.call_count == 1

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    @patch("hermes_cli.web_server_gateway._ACTION_PROCS", {})
    def test_cooldown_survives_the_action_table_being_cleared(
        self, mock_spawn, mock_subcmd
    ):
        """The guard must not live in ``_ACTION_PROCS``.

        Completed action children get reaped out of that table, and a guard
        that disappears when the child is reaped is the bug this fixes.
        """
        import hermes_cli.web_server as web_server
        from hermes_cli.web_server import _spawn_gateway_restart

        mock_spawn.return_value = _exited_proc()

        with patch("hermes_cli.gateway._reap_unsupervised_gateway_orphans"), patch(
            "hermes_cli.web_server.time.monotonic", side_effect=[100.0, 102.0]
        ):
            _spawn_gateway_restart()
            _web_server_gateway._ACTION_PROCS.clear()
            _web_server_gateway._ACTION_COMMANDS.clear()
            _, reused = _spawn_gateway_restart()

        assert mock_spawn.call_count == 1
        assert reused is True


class TestCooldownReleases:
    """The window always expires; it never wedges the restart action."""

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    @patch("hermes_cli.web_server_gateway._ACTION_PROCS", {})
    def test_request_after_the_window_starts_a_real_restart(
        self, mock_spawn, mock_subcmd
    ):
        from hermes_cli.web_server import _spawn_gateway_restart

        mock_spawn.side_effect = [_exited_proc(1), _exited_proc(2)]

        with patch("hermes_cli.gateway._reap_unsupervised_gateway_orphans"), patch(
            "hermes_cli.web_server.time.monotonic",
            side_effect=[100.0, 111.0, 111.0],
        ):
            _spawn_gateway_restart()
            second, reused = _spawn_gateway_restart()

        assert mock_spawn.call_count == 2
        assert reused is False
        assert second.pid == 2

    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    @patch("hermes_cli.web_server_gateway._ACTION_PROCS", {})
    def test_a_different_profile_is_never_coalesced(self, mock_spawn):
        """Two profiles are two services; one's restart is not the other's."""
        from hermes_cli.web_server import _spawn_gateway_restart

        mock_spawn.side_effect = [_exited_proc(1), _exited_proc(2)]

        with patch("hermes_cli.gateway._reap_unsupervised_gateway_orphans"), patch(
            "hermes_cli.web_server.time.monotonic",
            side_effect=[100.0, 101.0],
        ), patch(
            "hermes_cli.web_server_gateway._gateway_subcommand",
            side_effect=[["gateway", "restart"], ["-p", "coder", "gateway", "restart"]],
        ):
            _spawn_gateway_restart()
            second, reused = _spawn_gateway_restart(profile="coder")

        assert mock_spawn.call_count == 2
        assert reused is False
        assert second.pid == 2


class TestExistingBehaviourIsPreserved:
    """Regression guards on the pre-existing in-flight reuse."""

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    def test_live_child_is_still_reused_without_consulting_the_clock(
        self, mock_spawn, mock_subcmd
    ):
        from hermes_cli.web_server import _spawn_gateway_restart

        live = MagicMock(spec=subprocess.Popen)
        live.poll.return_value = None
        live.pid = 7

        with patch(
            "hermes_cli.web_server_gateway._ACTION_PROCS", {"gateway-restart": live}
        ), patch(
            "hermes_cli.web_server_gateway._ACTION_COMMANDS",
            {"gateway-restart": ("gateway", "restart")},
        ), patch(
            "hermes_cli.gateway._reap_unsupervised_gateway_orphans"
        ):
            proc, reused = _spawn_gateway_restart()

        assert proc is live
        assert reused is True
        mock_spawn.assert_not_called()

    @patch(
        "hermes_cli.web_server_gateway._gateway_subcommand",
        return_value=["gateway", "restart"],
    )
    @patch("hermes_cli.web_server_gateway._spawn_hermes_action")
    def test_live_child_for_another_profile_still_raises(self, mock_spawn, mock_subcmd):
        from hermes_cli.web_server import _spawn_gateway_restart

        live = MagicMock(spec=subprocess.Popen)
        live.poll.return_value = None

        with patch(
            "hermes_cli.web_server_gateway._ACTION_PROCS", {"gateway-restart": live}
        ), patch(
            "hermes_cli.web_server_gateway._ACTION_COMMANDS",
            {"gateway-restart": ("-p", "coder", "gateway", "restart")},
        ), patch(
            "hermes_cli.gateway._reap_unsupervised_gateway_orphans"
        ):
            with pytest.raises(RuntimeError, match="another profile"):
                _spawn_gateway_restart()

        mock_spawn.assert_not_called()
