"""Focused tests for ``anima rm --all`` batch behavior.

Spec source: vectl step ``rm-batch.add-rm-all-tests``.
"""

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch

import pytest
from click.testing import CliRunner
from rich.console import Console
from returns.result import Failure, Success

from anima.core.schemas import AgentState, AgentStateData, JobSpec
from anima.shell.cli import main
from anima.shell.cli_commands import cleanup_cmds
from anima.shell.state import create_agent_state, read_state, write_state


@pytest.fixture
def cli_runner() -> CliRunner:
    """Create a CliRunner instance for CLI invocation tests."""
    return CliRunner()


def _create_agent(
    instance_id: str, persona_name: str = "test-persona"
) -> AgentStateData:
    """Create an agent on disk and return its state.

    Spec source: use existing ``create_agent_state`` pattern from ``tests/test_cli.py``.
    """
    job = JobSpec(persona_ref="test@test", instance_id=instance_id)
    created = create_agent_state(
        instance_id=instance_id, persona_name=persona_name, job=job
    )
    assert isinstance(created, Success), (
        f"Failed to create agent state: {created.failure()}"
    )
    return created.unwrap()


def _transition_state(
    agent: AgentStateData, state: AgentState, pid: int | None = None
) -> AgentStateData:
    """Transition an agent to the requested state and reload it.

    Spec source: rm tests in ``tests/test_cli.py`` transition via ``write_state``.
    """
    now = datetime.now(timezone.utc).isoformat()
    updated = AgentStateData(
        **{
            **agent.model_dump(),
            "state": state,
            "updated_at": now,
            "pid": pid,
        }
    )
    write_result = write_state(agent.instance_id, updated, "test_transition", {})
    assert isinstance(write_result, Success), (
        f"Failed to transition state: {write_result.failure()}"
    )
    state_result = read_state(agent.agent_id)
    assert isinstance(state_result, Success), (
        f"Failed to read state: {state_result.failure()}"
    )
    return state_result.unwrap()


class TestCliRmAllBatch:
    """Batch ``anima rm --all`` behavior from rm-batch.add-rm-all-tests."""

    def test_rm_all_terminal_only_delete(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """Delete only terminal agents without requiring --force."""
        completed = _transition_state(
            _create_agent("inst_rm_all_completed"), AgentState.completed
        )
        failed = _transition_state(
            _create_agent("inst_rm_all_failed"), AgentState.failed
        )

        result = cli_runner.invoke(
            main,
            ["rm", "--all", "--yes", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert payload["deleted"] == 2
        assert payload["killed_first"] == 0
        assert all(agent["action"] == "deleted" for agent in payload["agents"])

        assert isinstance(read_state(completed.instance_id), Failure)
        assert isinstance(read_state(failed.agent_id), Failure)

    def test_rm_all_skip_non_terminal_without_force(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """Skip non-terminal agents when --force is not provided."""
        running = _transition_state(
            _create_agent("inst_rm_all_running_skip"),
            AgentState.running,
            pid=12345,
        )

        result = cli_runner.invoke(
            main,
            ["rm", "--all", "--yes", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert payload == {"deleted": 0, "killed_first": 0, "agents": []}

        assert isinstance(read_state(running.instance_id), Success)

    def test_rm_all_force_kills_first_then_deletes(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """--force kills non-terminal agents before deleting them."""
        running = _transition_state(
            _create_agent("inst_rm_all_running_force"),
            AgentState.running,
            pid=12345,
        )
        completed = _transition_state(
            _create_agent("inst_rm_all_completed_force"), AgentState.completed
        )

        with patch("os.kill") as mock_kill:
            result = cli_runner.invoke(
                main,
                ["rm", "--all", "--force", "--yes", "--json"],
                env={"ANIMA_STATE": str(tmp_state_dir)},
            )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert payload["deleted"] == 2
        assert payload["killed_first"] == 1
        assert {agent["instance_id"] for agent in payload["agents"]} == {
            running.instance_id,
            completed.instance_id,
        }
        mock_kill.assert_called_once()

    def test_rm_all_json_structure(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """JSON output includes expected top-level and per-agent fields."""
        _transition_state(
            _create_agent("inst_rm_all_json_struct"), AgentState.completed
        )

        result = cli_runner.invoke(
            main,
            ["rm", "--all", "--yes", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert set(payload.keys()) == {"deleted", "killed_first", "agents"}
        assert isinstance(payload["deleted"], int)
        assert isinstance(payload["killed_first"], int)
        assert isinstance(payload["agents"], list)
        assert set(payload["agents"][0].keys()) == {
            "instance_id",
            "state",
            "action",
            "killed_first",
        }

    def test_rm_all_no_agents_behavior(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """No agents returns an empty rm-all summary."""
        result = cli_runner.invoke(
            main,
            ["rm", "--all", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert payload == {"deleted": 0, "killed_first": 0, "agents": []}

    def test_rm_all_mutual_exclusion_with_agent_id(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """Reject combining AGENT_ID with --all."""
        result = cli_runner.invoke(
            main,
            ["rm", "inst_123", "--all"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 1
        assert "Cannot use AGENT_ID with --all" in result.output

    def test_rm_all_confirmation_prompt_decline(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """Declining confirmation leaves eligible agents untouched."""
        completed = _transition_state(
            _create_agent("inst_rm_all_confirm_decline"), AgentState.completed
        )

        result = cli_runner.invoke(
            main,
            ["rm", "--all"],
            input="n\n",
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert "Cancelled." in result.output
        assert isinstance(read_state(completed.instance_id), Success)

    def test_rm_all_force_json_reports_killed_then_deleted_action(
        self,
        cli_runner: CliRunner,
        tmp_state_dir: Path,
    ) -> None:
        """--force --json reports action and killed_first counters."""
        running = _transition_state(
            _create_agent("inst_rm_all_force_json_action"),
            AgentState.running,
            pid=12345,
        )

        with patch("os.kill"):
            result = cli_runner.invoke(
                main,
                ["rm", "--all", "--force", "--yes", "--json"],
                env={"ANIMA_STATE": str(tmp_state_dir)},
            )

        assert result.exit_code == 0
        payload = json.loads(result.output.strip())
        assert payload["killed_first"] == 1
        assert len(payload["agents"]) == 1
        assert payload["agents"][0]["instance_id"] == running.instance_id
        assert payload["agents"][0]["action"] == "killed_then_deleted"
        assert payload["agents"][0]["killed_first"] is True


def test_handle_non_terminal_agent_returns_after_error_when_sys_exit_intercepted(
    tmp_state_dir: Path,
) -> None:
    """Spec source: fix-unsafe-unwrap-after-sys-exit (transition failure path)."""
    running = _transition_state(
        _create_agent("inst_rm_all_transition_failure_guard"),
        AgentState.running,
        pid=12345,
    )
    console = Console(record=True)

    with (
        patch.object(cleanup_cmds, "transition_state", return_value=Failure("boom")),
        patch.object(cleanup_cmds, "_print_error", return_value=None),
    ):
        cleanup_cmds._handle_non_terminal_agent(
            console=console,
            json_output=True,
            state=running,
            force=True,
        )


def test_rm_bulk_returns_after_error_when_sys_exit_intercepted() -> None:
    """Spec source: post-review-fixes.fix-rm-bulk-exit-behavior-test-order-shared-state-should-fix."""
    console = Console(record=True)
    running = _transition_state(
        _create_agent("inst_rm_all_bulk_transition_failure_guard"),
        AgentState.running,
        pid=12345,
    )

    with (
        patch.object(cleanup_cmds, "list_agents", return_value=Success([running])),
        patch.object(
            cleanup_cmds,
            "_handle_non_terminal_agent",
            return_value=False,
        ) as mock_handle_non_terminal,
        patch.object(cleanup_cmds, "delete_agent_state") as mock_delete,
        patch.object(cleanup_cmds.sys, "exit") as mock_exit,
    ):
        cleanup_cmds._rm_bulk(force=True, yes=True, json_output=True, console=console)

    mock_handle_non_terminal.assert_called_once_with(
        console=console,
        json_output=True,
        state=running,
        force=True,
        killable=cleanup_cmds.killable_states(),
    )
    mock_exit.assert_not_called()
    mock_delete.assert_not_called()


def test_rm_impl_delete_failure_does_not_fall_through_when_sys_exit_intercepted() -> (
    None
):
    """Spec source: post-review-fixes.retest-gate-review-fixes-retest-shared-helper-location-fix-control-flow-guard."""
    console = Console(record=True)
    completed = _transition_state(
        _create_agent("inst_rm_delete_failure_control_flow_guard"),
        AgentState.completed,
    )

    with (
        patch.object(cleanup_cmds, "delete_agent_state", return_value=Failure("boom")),
        patch.object(
            cleanup_cmds, "_print_error", return_value=None
        ) as mock_print_error,
        patch.object(cleanup_cmds, "_print_rm_success") as mock_print_success,
        patch.object(cleanup_cmds.sys, "exit") as mock_exit,
    ):
        # Compatibility-only: agent_id parameter is an alias for instance_id
        cleanup_cmds.rm_impl(
            agent_id=completed.instance_id,  # compatibility-only: display alias
            force=False,
            json_output=True,
            console=console,
        )

    mock_print_error.assert_called_once()
    mock_print_success.assert_not_called()
    mock_exit.assert_not_called()
