"""End-to-end test for stale state detection in anima list.

Tests verification that `anima list` correctly detects and auto-reaps
running agents with dead PIDs, transitioning them to failed state.

Spec source: e2e-coverage.stale-state-detection
"""

import json
import os
from datetime import datetime, timezone
from pathlib import Path

import pytest
from click.testing import CliRunner
from returns.result import Success

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


class TestStaleStateDetection:
    """L3 E2E tests for stale state detection in 'anima list' command.

    Tests that `anima list`:
    1. Detects running agents with dead PIDs
    2. Auto-reaps stale agents (running -> failed)
    3. Shows accurate failed state to users (not noisy running)
    """

    @pytest.fixture
    def cli_runner(self) -> CliRunner:
        """Create a CliRunner instance for E2E testing."""
        return CliRunner()

    def _get_dead_pid(self) -> int:
        """Find a PID that definitely doesn't exist.

        Uses os.kill(pid, 0) to probe for non-existent PIDs.

        Returns:
            A PID that is guaranteed not to exist.
        """
        # Try to find a dead PID in a reasonable range
        for pid in range(30000, 40000):
            try:
                os.kill(pid, 0)
            except OSError as e:
                if e.errno == 3:  # ESRCH - no such process
                    return pid
        # Fallback: use a very high PID unlikely to exist
        return 999999

    def test_list_detects_and_reaps_stale_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima list` with running agent having dead PID -> auto-reap shows failed state.

        Verification steps:
        1. Create running agent with dead PID manually
        2. Call `anima list --json`
        3. Verify agent state is now 'failed' (not 'running')
        4. Verify error message mentions 'stale'
        5. Verify state.json on disk reflects failed state
        """
        # Create agent in running state with dead PID
        job = JobSpec(persona_ref="test@test", instance_id="inst_stale_test")
        create_result = create_agent_state("inst_stale_test", "stale-persona", job)
        assert isinstance(create_result, Success), "Failed to create agent"
        agent = create_result.unwrap()

        # Transition to running state with dead PID
        dead_pid = self._get_dead_pid()
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{
                **agent.model_dump(),
                "state": AgentState.running,
                "pid": dead_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent.instance_id, running_state, "start", {"pid": dead_pid})

        # Verify initial state is running
        initial_result = read_state(agent.instance_id)
        assert isinstance(initial_result, Success)
        initial_state = initial_result.unwrap()
        assert initial_state.state == AgentState.running
        assert initial_state.pid == dead_pid

        # Call `anima list --json`
        result = cli_runner.invoke(
            main,
            ["list", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0, f"Command failed: {result.output}"

        # Parse output
        agents = json.loads(result.output.strip())
        assert len(agents) == 1, "Should have exactly one agent"

        # Verify agent is now failed (auto-reaped)
        reaped_agent = agents[0]
        assert reaped_agent["instance_id"] == agent.instance_id
        assert reaped_agent["state"] == "failed", (
            f"Expected failed state, got {reaped_agent['state']}"
        )
        assert reaped_agent["error"] is not None, (
            "Failed agent should have error message"
        )
        assert "stale" in reaped_agent["error"].lower(), (
            f"Error should mention 'stale', got: {reaped_agent['error']}"
        )

        # Verify on-disk state reflects failed state
        final_result = read_state(agent.instance_id)
        assert isinstance(final_result, Success)
        final_state = final_result.unwrap()
        assert final_state.state == AgentState.failed
        assert "stale" in final_state.error.lower() if final_state.error else False

    def test_list_shows_alive_running_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima list` with running agent having alive PID -> shows running state unchanged.

        Verification steps:
        1. Create running agent with alive PID (current process)
        2. Call `anima list --json`
        3. Verify agent state remains 'running' (not reaped)
        """
        # Create agent in running state with current PID (alive)
        job = JobSpec(persona_ref="test@test", instance_id="inst_alive_test")
        create_result = create_agent_state("inst_alive_test", "alive-persona", job)
        assert isinstance(create_result, Success)
        agent = create_result.unwrap()

        # Transition to running with current PID (definitely alive)
        alive_pid = os.getpid()
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{
                **agent.model_dump(),
                "state": AgentState.running,
                "pid": alive_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent.instance_id, running_state, "start", {"pid": alive_pid})

        # Verify initial state is running
        initial_result = read_state(agent.instance_id)
        assert isinstance(initial_result, Success)
        initial_state = initial_result.unwrap()
        assert initial_state.state == AgentState.running
        assert initial_state.pid == alive_pid

        # Call `anima list --json`
        result = cli_runner.invoke(
            main,
            ["list", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0

        # Parse output
        agents = json.loads(result.output.strip())
        assert len(agents) == 1

        # Verify agent is still running (not reaped)
        alive_agent = agents[0]
        assert alive_agent["instance_id"] == agent.instance_id
        assert alive_agent["state"] == "running", (
            f"Expected running state, got {alive_agent['state']}"
        )
        assert alive_agent.get("error") is None, "Running agent should not have error"

        # Verify on-disk state unchanged
        final_result = read_state(agent.instance_id)
        assert isinstance(final_result, Success)
        final_state = final_result.unwrap()
        assert final_state.state == AgentState.running
        assert final_state.error is None

    def test_list_mixed_agents_shows_correct_states(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima list` with mix of running (dead PID), running (alive PID), and completed agents.

        Verification steps:
        1. Create 3 agents: running with dead PID, running with alive PID, completed
        2. Call `anima list --json`
        3. Verify states: failed, running, completed
        4. Only stale agent is reaped, others unchanged
        """
        # Agent 1: Running with dead PID (should be reaped -> failed)
        job1 = JobSpec(persona_ref="test@test", instance_id="inst_mixed_stale")
        create_result1 = create_agent_state("inst_mixed_stale", "stale-persona", job1)
        assert isinstance(create_result1, Success)
        agent1 = create_result1.unwrap()

        dead_pid = self._get_dead_pid()
        now = datetime.now(timezone.utc).isoformat()
        stale_state = AgentStateData(
            **{
                **agent1.model_dump(),
                "state": AgentState.running,
                "pid": dead_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent1.instance_id, stale_state, "start", {"pid": dead_pid})

        # Agent 2: Running with alive PID (should stay running)
        job2 = JobSpec(persona_ref="test@test", instance_id="inst_mixed_alive")
        create_result2 = create_agent_state("inst_mixed_alive", "alive-persona", job2)
        assert isinstance(create_result2, Success)
        agent2 = create_result2.unwrap()

        alive_pid = os.getpid()
        alive_state = AgentStateData(
            **{
                **agent2.model_dump(),
                "state": AgentState.running,
                "pid": alive_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent2.instance_id, alive_state, "start", {"pid": alive_pid})

        # Agent 3: Completed (should stay completed)
        job3 = JobSpec(persona_ref="test@test", instance_id="inst_mixed_completed")
        create_result3 = create_agent_state(
            "inst_mixed_completed", "completed-persona", job3
        )
        assert isinstance(create_result3, Success)
        agent3 = create_result3.unwrap()

        completed_state = AgentStateData(
            **{
                **agent3.model_dump(),
                "state": AgentState.completed,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent3.instance_id, completed_state, "finish", {})

        # Call `anima list --json`
        result = cli_runner.invoke(
            main,
            ["list", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0

        # Parse output
        agents = json.loads(result.output.strip())
        assert len(agents) == 3

        # Build lookup by instance_id
        agents_by_id = {a["instance_id"]: a for a in agents}

        # Agent 1: Should be failed (stale -> reaped)
        assert agents_by_id[agent1.instance_id]["state"] == "failed"
        assert "stale" in agents_by_id[agent1.instance_id]["error"].lower()

        # Agent 2: Should still be running (alive)
        assert agents_by_id[agent2.instance_id]["state"] == "running"
        assert agents_by_id[agent2.instance_id].get("error") is None

        # Agent 3: Should still be completed
        assert agents_by_id[agent3.instance_id]["state"] == "completed"
        assert agents_by_id[agent3.instance_id].get("error") is None

    def test_list_state_filter_with_stale_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima list --state running` with dead PID agent -> shows agent as failed after reaping.

        Verification steps:
        1. Create running agent with dead PID
        2. Call `anima list --state running`
        3. Verify agent appears in list but state is failed (reaped from running)

        Note: State filter is applied BEFORE stale detection in current implementation.
        So --state running filters agents initially in running state, then stale
        detection reaps them to failed state (still in the result list).
        """
        # Create running agent with dead PID
        job = JobSpec(persona_ref="test@test", instance_id="inst_filter_stale")
        create_result = create_agent_state("inst_filter_stale", "filter-persona", job)
        assert isinstance(create_result, Success)
        agent = create_result.unwrap()

        dead_pid = self._get_dead_pid()
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{
                **agent.model_dump(),
                "state": AgentState.running,
                "pid": dead_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent.instance_id, running_state, "start", {"pid": dead_pid})

        # Call `anima list --state running --json`
        result = cli_runner.invoke(
            main,
            ["list", "--state", "running", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0

        # Parse output
        agents = json.loads(result.output.strip())

        # Agent appears in list (matched running filter before reaping)
        # but state is now failed (reaped by stale detection)
        assert len(agents) == 1, f"Expected 1 agent, got {len(agents)}"
        reaped_agent = agents[0]
        assert reaped_agent["instance_id"] == agent.instance_id
        assert reaped_agent["state"] == "failed", (
            f"Expected failed state after reaping, got {reaped_agent['state']}"
        )
        assert "stale" in reaped_agent["error"].lower(), (
            f"Expected stale error message, got {reaped_agent['error']}"
        )

    def test_tree_command_reaps_stale_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima tree` with running agent having dead PID -> auto-reap shows failed state.

        Verification that `anima tree` also applies stale detection (uses same helper).
        """
        # Create running agent with dead PID
        job = JobSpec(persona_ref="test@test", instance_id="inst_tree_stale")
        create_result = create_agent_state("inst_tree_stale", "tree-persona", job)
        assert isinstance(create_result, Success)
        agent = create_result.unwrap()

        dead_pid = self._get_dead_pid()
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{
                **agent.model_dump(),
                "state": AgentState.running,
                "pid": dead_pid,
                "lifecycle": LifecycleType.ephemeral,
                "updated_at": now,
            }
        )
        write_state(agent.instance_id, running_state, "start", {"pid": dead_pid})

        # Call `anima tree --json`
        result = cli_runner.invoke(
            main,
            ["tree", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0

        # Parse output (tree returns flat list when no hierarchy)
        agents = json.loads(result.output.strip())
        assert len(agents) == 1

        # Verify agent is failed (auto-reaped)
        reaped_agent = agents[0]
        assert reaped_agent["instance_id"] == agent.instance_id
        assert reaped_agent["state"] == "failed"
        assert "stale" in reaped_agent["error"].lower()
