"""Integration tests for runner error cleanup behavior.

Spec link: vectl step integration-test-seams.test-error-cleanup.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

from returns.result import Success

from anima.core.lifecycle import LifecycleEvent
from anima.core.schemas import JobSpec
from anima.shell.state import create_agent_state, transition_state


REPO_ROOT = Path(__file__).resolve().parents[1]
RUNNER_PATH = REPO_ROOT / "src" / "anima" / "shell" / "agent_runner.py"


def _init_running_state(instance_id: str) -> Path:
    """Create state.json initialized with state=running for instance."""
    job = JobSpec(persona_ref="test@test", instance_id=instance_id)
    created = create_agent_state(instance_id, "test-persona", job)
    assert isinstance(created, Success)

    started = transition_state(instance_id, LifecycleEvent.start)
    assert isinstance(started, Success)

    anima_state = Path(os.environ["ANIMA_STATE"])
    state_file = anima_state / "agents" / instance_id / "state.json"
    assert state_file.exists(), f"Missing state file for {instance_id}: {state_file}"
    return state_file


def _load_state_on_disk(state_file: Path) -> dict[str, object]:
    return json.loads(state_file.read_text())


def _run_runner(
    instance_id: str,
    *args: str,
    env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
    full_env = os.environ.copy()
    if env:
        full_env.update(env)
    return subprocess.run(
        [sys.executable, str(RUNNER_PATH), instance_id, "test task", *args],
        capture_output=True,
        text=True,
        env=full_env,
        check=False,
    )


def _assert_failed_with_error(state_file: Path, case: str) -> None:
    state = _load_state_on_disk(state_file)
    assert state.get("state") == "failed", (
        f"{case}: expected state='failed', got {state.get('state')!r}"
    )
    assert state.get("error"), (
        f"{case}: expected non-empty error field, got {state.get('error')!r}"
    )


def _patch_env(tmp_path: Path, body: str) -> dict[str, str]:
    patch_dir = tmp_path / "sitepatch"
    patch_dir.mkdir(parents=True, exist_ok=True)
    (patch_dir / "sitecustomize.py").write_text(body)

    src_dir = str(REPO_ROOT / "src")
    existing = os.environ.get("PYTHONPATH", "")
    joined = os.pathsep.join(p for p in [str(patch_dir), src_dir, existing] if p)
    return {"PYTHONPATH": joined}


def test_t8_missing_persona_file_marks_state_failed() -> None:
    """T8: Missing persona file leaves state.json in failed state with error."""
    state_file = _init_running_state("inst_t8_missing_persona")

    result = _run_runner(
        "inst_t8_missing_persona",
        "--persona-file",
        "/definitely/missing/persona.json",
    )

    assert result.returncode != 0, "T8: runner should fail for missing persona file"
    _assert_failed_with_error(state_file, "T8")


def test_t9_corrupt_persona_json_marks_state_failed(tmp_path: Path) -> None:
    """T9: Corrupt persona JSON leaves state.json in failed state with error."""
    state_file = _init_running_state("inst_t9_bad_persona_json")
    persona_file = tmp_path / "persona.json"
    persona_file.write_text("{not-valid-json")

    result = _run_runner(
        "inst_t9_bad_persona_json",
        "--persona-file",
        str(persona_file),
    )

    assert result.returncode != 0, "T9: runner should fail for corrupt persona JSON"
    _assert_failed_with_error(state_file, "T9")


def test_t10_invalid_mcp_marks_state_failed() -> None:
    """T10: Invalid --mcp value leaves state.json in failed state with error."""
    state_file = _init_running_state("inst_t10_invalid_mcp")

    result = _run_runner(
        "inst_t10_invalid_mcp",
        "--mcp",
        "unterminated 'quote",
    )

    assert result.returncode != 0, "T10: runner should fail for invalid --mcp"
    _assert_failed_with_error(state_file, "T10")


def test_t11_run_agent_sync_failure_result_marks_state_failed(tmp_path: Path) -> None:
    """T11: run_agent_sync Failure result leaves state.json failed with error."""
    state_file = _init_running_state("inst_t11_run_agent_sync_failure")
    env_patch = _patch_env(
        tmp_path,
        "\n".join(
            [
                "from returns.result import Failure",
                "from anima.core.schemas import InternalError",
                "import anima.shell.agent as agent_mod",
                "",
                "async def _forced_run_failure(self):",
                "    return Failure(InternalError('forced failure for T11'))",
                "",
                "agent_mod.AgentRunner.run = _forced_run_failure",
            ]
        ),
    )

    result = _run_runner("inst_t11_run_agent_sync_failure", env=env_patch)

    assert result.returncode != 0, (
        "T11: runner should fail for run_agent_sync Failure path"
    )
    _assert_failed_with_error(state_file, "T11")


def test_t12_unhandled_exception_marks_state_failed(tmp_path: Path) -> None:
    """T12: unhandled run_agent_sync exception still marks state.json failed."""
    state_file = _init_running_state("inst_t12_unhandled_exception")
    env_patch = _patch_env(
        tmp_path,
        "\n".join(
            [
                "import anima.shell.agent as agent_mod",
                "",
                "def _boom(*args, **kwargs):",
                "    raise RuntimeError('forced unhandled exception for T12')",
                "",
                "agent_mod.run_agent_sync = _boom",
            ]
        ),
    )

    result = _run_runner("inst_t12_unhandled_exception", env=env_patch)

    assert result.returncode != 0, "T12: runner should fail on unhandled exception"
    _assert_failed_with_error(state_file, "T12")
