"""`petctl self_restart` — the pet restarting itself and remembering why.

Everything here runs against a temp context file and a fake subprocess runner,
so the tests exercise the arming/preflight/report logic without any process
actually dying.
"""

import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from bolt_pet import pet_actions, self_restart


@pytest.fixture
def state(tmp_path):
    return tmp_path / "restart_context.json"


def _ok_run(*args, **kwargs):
    return SimpleNamespace(returncode=0, stdout="", stderr="")


def _broken_run(*args, **kwargs):
    return SimpleNamespace(
        returncode=1, stdout="",
        stderr='  File "bolt_pet/controller.py", line 42\n    def _speak(\nSyntaxError: invalid syntax',
    )


# ── parsing ─────────────────────────────────────────────────────────────────

def test_self_restart_parses_with_a_free_text_reason():
    action = pet_actions.parse("petctl self_restart check the new walk cycle loads")
    assert action == {
        "action": "self_restart", "reason": "check the new walk cycle loads",
    }


def test_self_restart_needs_no_reason_and_accepts_aliases():
    assert pet_actions.parse("petctl self_restart")["reason"] == ""
    assert pet_actions.parse("petctl restart")["action"] == "self_restart"
    assert pet_actions.parse("petctl reboot")["action"] == "self_restart"


def test_self_restart_is_listed_in_the_help():
    assert "self_restart" in pet_actions.HELP


# ── preflight ───────────────────────────────────────────────────────────────

def test_preflight_passes_when_the_code_imports():
    self_restart.preflight(run=_ok_run)  # no exception


def test_preflight_hands_back_the_traceback_instead_of_dying(state):
    """The whole point: a syntax error Bolt just introduced comes back as
    something he can read and fix, in the same turn, with the pet still up."""
    with pytest.raises(self_restart.RestartError) as excinfo:
        self_restart.preflight(run=_broken_run)
    message = str(excinfo.value)
    assert "does not import" in message
    assert "SyntaxError" in message and "controller.py" in message


def test_preflight_runs_the_import_in_a_subprocess_not_here():
    """This process holds the *old* modules, so an in-process import would
    pass on a file that no longer parses."""
    seen = {}

    def capture(cmd, **kwargs):
        seen["cmd"], seen["kwargs"] = cmd, kwargs
        return SimpleNamespace(returncode=0, stdout="", stderr="")

    self_restart.preflight(run=capture)
    assert seen["cmd"][0] == sys.executable
    assert "import bolt_pet" in seen["cmd"][2]
    assert seen["kwargs"]["env"]["QT_QPA_PLATFORM"] == "offscreen"  # imports need no display


def test_a_subprocess_that_cannot_even_run_is_reported(monkeypatch):
    def explode(*args, **kwargs):
        raise OSError("no python here")

    with pytest.raises(self_restart.RestartError, match="couldn't run the preflight"):
        self_restart.preflight(run=explode)


# ── context across the restart ──────────────────────────────────────────────

def test_arming_persists_the_reason_for_the_next_process(state):
    self_restart.arm("check the sprite frames load", version="0.2.3",
                     session="pet-desktop", recent=["you: reload the sprites"],
                     path=state, now=1000.0)
    revived = self_restart.load(state)
    assert revived.reason == "check the sprite frames load"
    assert revived.version == "0.2.3"
    assert revived.recent == ["you: reload the sprites"]
    assert revived.restarts == [1000.0]


def test_no_context_means_a_normal_start(state):
    assert self_restart.load(state) is None


def test_a_corrupt_context_file_is_ignored_not_fatal(state):
    state.write_text("{not json at all", encoding="utf-8")
    assert self_restart.load(state) is None


def test_clearing_the_context_stops_it_being_re_announced(state):
    self_restart.arm("once", path=state, now=1000.0)
    self_restart.clear(state)
    assert self_restart.load(state) is None
    self_restart.clear(state)  # clearing twice is not an error


def test_the_report_says_what_happened_and_what_to_check(state):
    context = self_restart.arm(
        "verify the dialogue command works", verify="verify the dialogue command works",
        version="0.2.3", recent=["you: try a scene"], path=state, now=1000.0,
    )
    text = self_restart.report(context, version="0.2.4", now=1004.5)
    assert "I restarted myself" in text
    assert "verify the dialogue command works" in text
    assert "4.5s" in text
    assert "0.2.4" in text and "was 0.2.3" in text
    assert "you: try a scene" in text


# ── loop guard ──────────────────────────────────────────────────────────────

def test_restart_history_accumulates_across_restarts(state):
    self_restart.arm("one", path=state, now=1000.0)
    self_restart.arm("two", path=state, now=1100.0)
    assert self_restart.load(state).restarts == [1000.0, 1100.0]


def test_too_many_restarts_in_the_window_is_refused(state):
    now = 1000.0
    for index in range(self_restart.MAX_RESTARTS):
        self_restart.arm(f"attempt {index}", path=state, now=now + index)
    with pytest.raises(self_restart.RestartError, match="looping"):
        self_restart.check_loop_guard(self_restart.load(state), now=now + 10)


def test_old_restarts_fall_out_of_the_window(state):
    now = 1000.0
    for index in range(self_restart.MAX_RESTARTS):
        self_restart.arm(f"attempt {index}", path=state, now=now + index)
    later = now + self_restart.WINDOW_SECONDS + 60
    self_restart.check_loop_guard(self_restart.load(state), now=later)  # no exception
    assert self_restart.recent_restarts(self_restart.load(state), now=later) == []
