"""Seam tests for context-bundle wiring across runner/load/spawn."""

from __future__ import annotations

import json
import os
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast

import pytest
from returns.result import Failure, Success

from anima.core.schemas import (
    ContextMode,
    JobSpec,
    LifecycleType,
    NotFoundError,
    PersonaSpec,
)


@pytest.fixture(autouse=True)
def _stub_spawn_launcher(monkeypatch: pytest.MonkeyPatch) -> None:
    """Keep spawn tests on filesystem seams, not subprocesses."""

    def _launch_stub(**_kwargs: object) -> Success[int]:
        return Success(4242)

    monkeypatch.setattr("anima.shell.ctl_spawn.launch_agent_subprocess", _launch_stub)


class _EmptyRun:
    """Minimal async run object returned by FakeAgent.iter()."""

    async def __aenter__(self) -> _EmptyRun:
        return self

    async def __aexit__(self, *_exc: object) -> None:
        return None

    def __aiter__(self) -> _EmptyRun:
        return self

    async def __anext__(self) -> object:
        raise StopAsyncIteration

    def usage(self) -> None:
        return None

    def all_messages(self) -> list[object]:
        return []


class _CaptureIterAgent:
    """Fake agent that captures kwargs passed to iter()."""

    def __init__(self) -> None:
        self.iter_kwargs: dict[str, Any] | None = None

    def iter(self, **kwargs: Any) -> _EmptyRun:
        self.iter_kwargs = kwargs
        return _EmptyRun()


class TestAgentRunnerContextBundleSeam:
    """AgentRunner seam tests with real bundle files and real load paths."""

    @pytest.mark.asyncio
    async def test_run_agent_loop_uses_real_bundle_file_without_loader_patch(
        self, tmp_state_dir: Path
    ) -> None:
        """Shared-mode runner loads history from a real on-disk bundle file."""
        from anima.shell.agent import AgentRunner

        bundle_path = tmp_state_dir / "bundle.json"
        bundle_path.write_text(
            json.dumps(
                {
                    "bundle_id": "ctx_primary",
                    "raw_tail": [
                        {"role": "user", "content": "hello from parent"},
                        {"role": "assistant", "content": "ack from parent"},
                    ],
                }
            )
        )

        runner = AgentRunner(
            instance_id="runner_primary",
            task="run once",
            job=JobSpec(
                persona_ref="test@v1",
                instance_id="runner_primary",
                context_mode=ContextMode.shared,
                context_bundle_ref=str(bundle_path),
            ),
        )
        fake_agent = _CaptureIterAgent()
        state = SimpleNamespace(lifecycle=LifecycleType.ephemeral)

        result = await runner._run_agent_loop(cast(Any, fake_agent), state)

        assert isinstance(result, Success)
        assert fake_agent.iter_kwargs is not None
        loaded_history = fake_agent.iter_kwargs["message_history"]
        assert len(loaded_history) == 2
        assert loaded_history[0].parts[0].content == "hello from parent"
        assert loaded_history[1].parts[0].content == "ack from parent"

    @pytest.mark.asyncio
    async def test_run_agent_loop_returns_not_found_for_missing_bundle_path(
        self, tmp_state_dir: Path
    ) -> None:
        """Missing context_bundle_ref returns real NotFoundError failure."""
        from anima.shell.agent import AgentRunner

        missing_path = tmp_state_dir / "does-not-exist.json"
        runner = AgentRunner(
            instance_id="runner_missing_bundle",
            task="run once",
            job=JobSpec(
                persona_ref="test@v1",
                instance_id="runner_missing_bundle",
                context_mode=ContextMode.shared,
                context_bundle_ref=str(missing_path),
            ),
        )
        fake_agent = _CaptureIterAgent()
        state = SimpleNamespace(lifecycle=LifecycleType.ephemeral)

        result = await runner._run_agent_loop(cast(Any, fake_agent), state)

        assert isinstance(result, Failure)
        assert isinstance(result.failure(), NotFoundError)
        assert fake_agent.iter_kwargs is None


class TestContextBundleLoadPathResolution:
    """Path-resolution tests for context_bundle_ref handling."""

    def test_load_supports_absolute_and_relative_bundle_paths(
        self, tmp_path: Path
    ) -> None:
        """Loader resolves both absolute path and cwd-relative path correctly."""
        from anima.shell.context_bundle import load_message_history_from_context_bundle

        bundle_path = tmp_path / "nested" / "context_bundle.json"
        bundle_path.parent.mkdir(parents=True, exist_ok=True)
        bundle_path.write_text(
            json.dumps(
                {
                    "bundle_id": "ctx_paths",
                    "raw_tail": [
                        {"role": "user", "content": "path-check-user"},
                        {"role": "assistant", "content": "path-check-assistant"},
                    ],
                }
            )
        )

        absolute_result = load_message_history_from_context_bundle(str(bundle_path))
        assert isinstance(absolute_result, Success)
        absolute_history = absolute_result.unwrap()
        assert len(absolute_history) == 2

        old_cwd = os.getcwd()
        os.chdir(tmp_path)
        try:
            relative_result = load_message_history_from_context_bundle(
                "nested/context_bundle.json"
            )
        finally:
            os.chdir(old_cwd)

        assert isinstance(relative_result, Success)
        relative_history = relative_result.unwrap()
        assert len(relative_history) == 2
        assert relative_history[0].parts[0].content == "path-check-user"
        assert relative_history[1].parts[0].content == "path-check-assistant"


class TestCtlSpawnContextBundleSeam:
    """ctl_spawn seam tests with real persisted state and log files."""

    @pytest.mark.asyncio
    async def test_spawn_shared_writes_bundle_from_persisted_runtime_log(
        self, tmp_state_dir: Path
    ) -> None:
        """Shared spawn exports child bundle from persisted parent log.jsonl."""
        from anima.shell.ctl_spawn import ctl_spawn_subagent
        from anima.shell.state import (
            AgentState,
            AgentStateData,
            append_log,
            get_agent_dir,
            write_state,
        )

        parent_id = "parent_runtime"
        parent_persona = PersonaSpec(
            id="parent",
            description="A test persona.",
            prompt="parent",
            model="gpt-5.2",
            can_spawn=True,
        )
        child_persona = PersonaSpec(
            id="child",
            description="A test persona.",
            prompt="child",
            model="gpt-5.2",
        )

        old_cwd = os.getcwd()
        os.chdir(tmp_state_dir)
        try:
            parent_ref = "personas/parent.json"
            child_ref = "personas/child.json"
            Path(parent_ref).parent.mkdir(parents=True, exist_ok=True)
            Path(parent_ref).write_text(parent_persona.model_dump_json())
            Path(child_ref).write_text(child_persona.model_dump_json())

            write_state(
                parent_id,
                AgentStateData(
                    instance_id=parent_id,
                    trace_id="tr-runtime",
                    persona_name="parent",
                    persona_ref=parent_ref,
                    state=AgentState.running,
                    depth=0,
                    max_depth=3,
                ),
                "created",
            )

            append_log(parent_id, "interact_message", {"message": "first user"})
            append_log(parent_id, "response", {"response_text": "first assistant"})
            append_log(parent_id, "tool_call", {"name": "ignored_tool"})

            spawn_result = await ctl_spawn_subagent(
                instance_id=parent_id,
                persona_ref=child_ref,
                task="shared run",
                context_mode="shared",
            )
            assert isinstance(spawn_result, Success)
            child_id = spawn_result.unwrap()

            bundle_path = get_agent_dir(child_id) / "context_bundle.json"
            assert bundle_path.exists()
            bundle = json.loads(bundle_path.read_text())

            assert bundle["inherited_from"] == parent_id
            assert [entry["role"] for entry in bundle["raw_tail"]] == [
                "user",
                "assistant",
            ]
            assert [entry["content"] for entry in bundle["raw_tail"]] == [
                "first user",
                "first assistant",
            ]
        finally:
            os.chdir(old_cwd)

    @pytest.mark.asyncio
    async def test_spawn_uses_latest_persisted_state_over_stale_snapshot(
        self, tmp_state_dir: Path
    ) -> None:
        """Child job constraints follow latest persisted parent state values."""
        from anima.shell.ctl_spawn import ctl_spawn_subagent
        from anima.shell.state import (
            AgentState,
            AgentStateData,
            read_state,
            write_state,
        )

        parent_id = "parent_state_precedence"
        parent_persona = PersonaSpec(
            id="parent",
            description="A test persona.",
            prompt="parent",
            model="gpt-5.2",
            can_spawn=True,
        )
        child_persona = PersonaSpec(
            id="child",
            description="A test persona.",
            prompt="child",
            model="gpt-5.2",
        )

        old_cwd = os.getcwd()
        os.chdir(tmp_state_dir)
        try:
            parent_ref = "personas/parent_precedence.json"
            child_ref = "personas/child_precedence.json"
            Path(parent_ref).parent.mkdir(parents=True, exist_ok=True)
            Path(parent_ref).write_text(parent_persona.model_dump_json())
            Path(child_ref).write_text(child_persona.model_dump_json())

            stale_state = AgentStateData(
                instance_id=parent_id,
                trace_id="tr-precedence",
                persona_name="parent",
                persona_ref=parent_ref,
                state=AgentState.running,
                depth=0,
                max_depth=4,
                max_turns=2,
                budget_total=20,
                grace_budget=3,
            )
            write_state(parent_id, stale_state, "created")

            refreshed_state = stale_state.model_copy(
                update={"max_turns": 9, "budget_total": 900, "grace_budget": 44}
            )
            write_state(parent_id, refreshed_state, "job_refreshed")

            spawn_result = await ctl_spawn_subagent(
                instance_id=parent_id,
                persona_ref=child_ref,
                task="check precedence",
                context_mode="isolated",
            )
            assert isinstance(spawn_result, Success)
            child_id = spawn_result.unwrap()

            child_state_result = read_state(child_id)
            assert isinstance(child_state_result, Success)
            child_state = child_state_result.unwrap()

            assert child_state.max_turns == 9
            assert child_state.budget_total == 900
            assert child_state.grace_budget == 44
        finally:
            os.chdir(old_cwd)
