"""End-to-end CLI lifecycle scenarios.

Tests exercise the FULL STACK -- CLI entry point through state persistence
and back.
Each test verifies state transitions as observable through CLI commands.

Test classes:
1. TestRunLifecycle - ephemeral agent run command
2. TestSpawnStatusKillLifecycle - background agent lifecycle
3. TestApproveRejectResumeLifecycle - approval workflow
4. TestCollectLifecycle - result harvesting

Acceptance rule for this file: no boundary patch-call markers.
Patch-heavy seam tests are re-scoped to tests/test_component_cli_boundary_seams.py.
"""

import json
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 (
    AgentResult,
    AgentState,
    AgentStateData,
    JobSpec,
    PersonaSpec,
    TokenUsage,
)
from anima.shell.cli import EXIT_AGENT_NOT_FOUND, main
from anima.shell.state import (
    create_agent_state,
    read_state,
    write_result,
    write_state,
)


# =============================================================================
# Fixtures
# =============================================================================


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


@pytest.fixture
def persona_file(tmp_path: Path) -> Path:
    """Create a test persona file."""
    persona = {
        "name": "e2e-persona",
        "prompt": "You are an E2E test assistant.",
        "model": "e2e-model",
        "spec_version": "0.1.0",
    }
    file_path = tmp_path / "persona.json"
    file_path.write_text(json.dumps(persona))
    return file_path


# =============================================================================
# TestRunLifecycle: Run command tests
# =============================================================================


class TestRunLifecycle:
    """L3 E2E tests for 'anima run' lifecycle scenarios."""

    def test_run_invalid_persona_exits_nonzero(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima run --persona nonexistent "task"` -> exit != 0."""
        result = cli_runner.invoke(
            main,
            ["run", "test task", "--persona", "/nonexistent/persona.json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code != 0


# =============================================================================
# TestSpawnStatusKillLifecycle: Background agent lifecycle
# =============================================================================


class TestSpawnStatusKillLifecycle:
    """L3 E2E tests for spawn/status/kill/list flows."""

    def test_status_shows_spawned_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """After spawn, `anima status <instance_id>` -> shows state, persona name."""
        # Create agent directly
        job = JobSpec(persona_ref="test@test", instance_id="inst_status")
        create_result = create_agent_state("inst_status", "status-persona", job)
        agent = create_result.unwrap()

        # Transition to running
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        result = cli_runner.invoke(
            main,
            ["status", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert agent.instance_id in result.output
        assert "running" in result.output.lower()
        assert "status-persona" in result.output

    def test_status_json_has_required_fields(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima status <id> --json` -> JSON with instance_id, state, persona_name,
        lifecycle, token_usage.
        """
        job = JobSpec(persona_ref="test@test", instance_id="inst_json_status")
        create_result = create_agent_state("inst_json_status", "json-persona", job)
        agent = create_result.unwrap()

        result = cli_runner.invoke(
            main,
            ["status", agent.instance_id, "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        output = json.loads(result.output.strip())

        # Required fields per docs/INTERFACES.md Section A
        assert "instance_id" in output
        assert "state" in output
        assert "persona_name" in output
        assert "lifecycle" in output
        assert "token_usage" in output
        assert output["instance_id"] == agent.instance_id

    def test_status_prefix_match(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima status <first-8-chars>` -> matches full instance_id."""
        job = JobSpec(persona_ref="test@test", instance_id="inst_prefix")
        create_result = create_agent_state("inst_prefix", "prefix-persona", job)
        agent = create_result.unwrap()

        # Use first 8 characters as prefix
        prefix = agent.instance_id[:8]

        result = cli_runner.invoke(
            main,
            ["status", prefix],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert agent.instance_id in result.output

    def test_list_shows_agents(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """After spawn 2 agents, `anima list` -> table with 2 rows."""
        # Create two agents
        job1 = JobSpec(persona_ref="test@test", instance_id="inst_list1")
        job2 = JobSpec(persona_ref="test@test", instance_id="inst_list2")

        result1 = create_agent_state("inst_list1", "list-persona-1", job1)
        result2 = create_agent_state("inst_list2", "list-persona-2", job2)

        assert isinstance(result1, Success)
        assert isinstance(result2, Success)

        result = cli_runner.invoke(
            main,
            ["list"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        # Two agents should be listed
        # JSON output is easier to verify
        result_json = cli_runner.invoke(
            main,
            ["list", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        output = json.loads(result_json.output.strip())
        assert len(output) == 2

    def test_list_state_filter(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Spawn 2 agents, kill one, `anima list --state running` -> 1 result."""
        # Create two agents, transition one to running, one to killed
        job1 = JobSpec(persona_ref="test@test", instance_id="inst_filter1")
        job2 = JobSpec(persona_ref="test@test", instance_id="inst_filter2")

        result1 = create_agent_state("inst_filter1", "filter-persona-1", job1)
        result2 = create_agent_state("inst_filter2", "filter-persona-2", job2)

        agent1 = result1.unwrap()
        agent2 = result2.unwrap()

        # Agent 1: running
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent1.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent1.instance_id, running_state, "start", {})

        # Agent 2: killed
        killed_state = AgentStateData(
            **{**agent2.model_dump(), "state": AgentState.killed, "updated_at": now}
        )
        write_state(agent2.instance_id, killed_state, "kill", {})

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

        assert result.exit_code == 0
        output = json.loads(result.output.strip())
        assert len(output) == 1
        assert output[0]["instance_id"] == agent1.instance_id


# =============================================================================
# TestApproveRejectResumeLifecycle: Approval workflow tests
# =============================================================================


class TestApproveRejectResumeLifecycle:
    """L3 E2E tests for approve/reject/resume flows."""

    def test_approve_flow(self, cli_runner: CliRunner, tmp_state_dir: Path) -> None:
        """Create agent in awaiting_approval state, `anima approve <id> --note "ok"`
        -> state=running.
        """
        # Create agent and transition to awaiting_approval
        job = JobSpec(persona_ref="test@test", instance_id="inst_approve")
        create_result = create_agent_state("inst_approve", "approve-persona", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        approval_state = AgentStateData(
            **{**running_state.model_dump(), "state": AgentState.awaiting_approval}
        )
        write_state(agent.instance_id, approval_state, "request_approval", {})

        result = cli_runner.invoke(
            main,
            ["approve", agent.instance_id, "--note", "ok"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert "running" in result.output.lower()

        # Verify state transition via filesystem
        state_result = read_state(agent.instance_id)
        assert isinstance(state_result, Success)
        updated = state_result.unwrap()
        assert updated.state == AgentState.running

    def test_reject_flow(self, cli_runner: CliRunner, tmp_state_dir: Path) -> None:
        """Create agent in awaiting_approval state, `anima reject <id> --reason "no"`
        -> state=failed.
        """
        # Create agent and transition to awaiting_approval
        job = JobSpec(persona_ref="test@test", instance_id="inst_reject")
        create_result = create_agent_state("inst_reject", "reject-persona", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        approval_state = AgentStateData(
            **{**running_state.model_dump(), "state": AgentState.awaiting_approval}
        )
        write_state(agent.instance_id, approval_state, "request_approval", {})

        result = cli_runner.invoke(
            main,
            ["reject", agent.instance_id, "--reason", "no"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert "running" in result.output.lower()

        # Verify state transition via filesystem
        state_result = read_state(agent.instance_id)
        assert isinstance(state_result, Success)
        updated = state_result.unwrap()
        assert updated.state == AgentState.running

    def test_resume_flow(self, cli_runner: CliRunner, tmp_state_dir: Path) -> None:
        """Create agent in suspended state, `anima resume <id>` -> state=running."""
        # Create agent and transition to suspended
        job = JobSpec(persona_ref="test@test", instance_id="inst_resume")
        create_result = create_agent_state("inst_resume", "resume-persona", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        suspended_state = AgentStateData(
            **{**running_state.model_dump(), "state": AgentState.suspended}
        )
        write_state(agent.instance_id, suspended_state, "suspend", {})

        result = cli_runner.invoke(
            main,
            ["resume", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert "running" in result.output.lower()

        # Verify state transition via filesystem
        state_result = read_state(agent.instance_id)
        assert isinstance(state_result, Success)
        updated = state_result.unwrap()
        assert updated.state == AgentState.running

    def test_approve_wrong_state_exits_nonzero(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Create running agent, `anima approve <id>` -> exit != 0 with error message."""
        # Create running agent (not awaiting_approval)
        job = JobSpec(persona_ref="test@test", instance_id="inst_wrong_state")
        create_result = create_agent_state("inst_wrong_state", "wrong-persona", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        result = cli_runner.invoke(
            main,
            ["approve", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code != 0
        assert "not awaiting approval" in result.output.lower()


# =============================================================================
# TestCollectLifecycle: Result harvesting tests
# =============================================================================


class TestCollectLifecycle:
    """L3 E2E tests for collect flows."""

    def test_collect_completed_agent(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Create completed agent with result, `anima collect <id>` -> exit 0,
        shows result.
        """
        # Create and complete agent
        job = JobSpec(persona_ref="test@test", instance_id="inst_collect")
        create_result = create_agent_state("inst_collect", "collect-persona", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        completed_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.completed, "updated_at": now}
        )
        write_state(agent.instance_id, completed_state, "finish", {})

        # Write result
        agent_result = AgentResult(
            final_state=AgentState.completed,
            token_usage=TokenUsage(prompt_tokens=100, completion_tokens=50),
            result_ref="results/test.json",
        )
        write_result(agent.instance_id, agent_result)

        result = cli_runner.invoke(
            main,
            ["collect", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        assert "completed" in result.output.lower()

    def test_collect_running_agent_no_wait(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Running agent, `anima collect <id>` -> exit 1 (not terminal)."""
        # Create running agent
        job = JobSpec(persona_ref="test@test", instance_id="inst_collect_running")
        create_result = create_agent_state(
            "inst_collect_running", "collect-running", job
        )
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        result = cli_runner.invoke(
            main,
            ["collect", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 1
        assert "still" in result.output.lower()

    def test_collect_json_output(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima collect <id> --json` -> JSON with all result fields."""
        # Create completed agent
        job = JobSpec(persona_ref="test@test", instance_id="inst_collect_json")
        create_result = create_agent_state("inst_collect_json", "collect-json", job)
        agent = create_result.unwrap()

        now = datetime.now(timezone.utc).isoformat()
        completed_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.completed, "updated_at": now}
        )
        write_state(agent.instance_id, completed_state, "finish", {})

        agent_result = AgentResult(
            final_state=AgentState.completed,
            token_usage=TokenUsage(prompt_tokens=200, completion_tokens=100),
            result_ref="results/json_test.json",
        )
        write_result(agent.instance_id, agent_result)

        result = cli_runner.invoke(
            main,
            ["collect", agent.instance_id, "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        assert result.exit_code == 0
        output = json.loads(result.output.strip())

        assert "instance_id" in output
        assert "final_state" in output
        assert "token_usage" in output
        assert output["final_state"] == "completed"
        assert output["token_usage"]["total_tokens"] == 300

    def test_collect_not_found(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """`anima collect nonexistent` -> exit code for not-found (POSIX valid, <= 255)."""
        result = cli_runner.invoke(
            main,
            ["collect", "nonexistent_agent_id"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )

        # EXIT_AGENT_NOT_FOUND = 2 per CLI
        assert result.exit_code == EXIT_AGENT_NOT_FOUND
        assert 0 < result.exit_code <= 255  # POSIX valid range


# =============================================================================
# Additional E2E scenarios
# =============================================================================


class TestAdditionalE2EScenarios:
    """Additional E2E CLI tests for edge cases and integration."""

    def test_state_persists_across_commands(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Verify state transitions persist across CLI commands via filesystem."""
        # Create agent
        job = JobSpec(persona_ref="test@test", instance_id="inst_persist")
        create_result = create_agent_state("inst_persist", "persist-persona", job)
        agent = create_result.unwrap()

        # Initial state: pending
        state1 = read_state(agent.instance_id).unwrap()
        assert state1.state == AgentState.pending

        # CLI: transition to running
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        # Verify via CLI
        status_result = cli_runner.invoke(
            main,
            ["status", agent.instance_id, "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        status_output = json.loads(status_result.output.strip())
        assert status_output["state"] == "running"

        # CLI: transition to suspended
        suspended_state = AgentStateData(
            **{**running_state.model_dump(), "state": AgentState.suspended}
        )
        write_state(agent.instance_id, suspended_state, "suspend", {})

        # Verify via direct filesystem read
        state2 = read_state(agent.instance_id).unwrap()
        assert state2.state == AgentState.suspended

        # CLI: resume back to running
        resume_result = cli_runner.invoke(
            main,
            ["resume", agent.instance_id],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        assert resume_result.exit_code == 0

        # Final verification via CLI
        status_result2 = cli_runner.invoke(
            main,
            ["status", agent.instance_id, "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        status_output2 = json.loads(status_result2.output.strip())
        assert status_output2["state"] == "running"

    def test_full_approve_reject_workflow(
        self, cli_runner: CliRunner, tmp_state_dir: Path
    ) -> None:
        """Complete approval workflow: create -> request_approval -> approve/reject."""
        # Create agent
        job = JobSpec(persona_ref="test@test", instance_id="inst_workflow")
        create_result = create_agent_state("inst_workflow", "workflow-persona", job)
        agent = create_result.unwrap()

        # Start -> running
        now = datetime.now(timezone.utc).isoformat()
        running_state = AgentStateData(
            **{**agent.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent.instance_id, running_state, "start", {})

        # Request approval -> awaiting_approval
        approval_state = AgentStateData(
            **{**running_state.model_dump(), "state": AgentState.awaiting_approval}
        )
        write_state(agent.instance_id, approval_state, "request_approval", {})

        # Verify state via CLI
        status1 = cli_runner.invoke(
            main,
            ["status", agent.instance_id, "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        assert json.loads(status1.output.strip())["state"] == "awaiting_approval"

        # Approve
        approve_result = cli_runner.invoke(
            main,
            ["approve", agent.instance_id, "--note", "Approved for testing", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        assert approve_result.exit_code == 0
        approve_output = json.loads(approve_result.output.strip())
        assert approve_output["state"] == "running"
        assert approve_output["approved"] is True

        # Verify state after approve
        state_after = read_state(agent.instance_id).unwrap()
        assert state_after.state == AgentState.running

        # Create another agent for reject flow
        job2 = JobSpec(persona_ref="test@test", instance_id="inst_workflow2")
        create_result2 = create_agent_state("inst_workflow2", "workflow2-persona", job2)
        agent2 = create_result2.unwrap()

        running_state2 = AgentStateData(
            **{**agent2.model_dump(), "state": AgentState.running, "updated_at": now}
        )
        write_state(agent2.instance_id, running_state2, "start", {})

        approval_state2 = AgentStateData(
            **{**running_state2.model_dump(), "state": AgentState.awaiting_approval}
        )
        write_state(agent2.instance_id, approval_state2, "request_approval", {})

        # Reject
        reject_result = cli_runner.invoke(
            main,
            ["reject", agent2.instance_id, "--reason", "Not approved", "--json"],
            env={"ANIMA_STATE": str(tmp_state_dir)},
        )
        assert reject_result.exit_code == 0
        reject_output = json.loads(reject_result.output.strip())
        assert reject_output["state"] == "running"
        assert reject_output["rejected"] is True

        # Verify state after reject
        state_after_reject = read_state(agent2.instance_id).unwrap()
        assert state_after_reject.state == AgentState.running
