"""Integration test for full compaction pipeline through AgentRunner.

Spec link:
- context-compaction.test-compaction-integration

This test is intentionally higher-level than unit tests in test_compaction.py:
- Uses real AgentRunner control loop
- Uses PydanticAI TestModel (no mocked model)
- Uses real state/log persistence via ANIMA_STATE temp dir
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest
from pydantic_ai.models.test import TestModel
from returns.result import Failure, Success

from anima.shell import agent as agent_module
from anima.core.lifecycle import LifecycleEvent
from anima.core.schemas import (
    AgentState,
    CompletionPolicy,
    ContextMode,
    JobSpec,
    PersonaSpec,
)
from anima.shell.agent import AgentRunner
from anima.shell.state import create_agent_state, read_log, read_state, transition_state
from tests.helpers.mcp_fakes import FakeMCPServer


def _has_compaction_summary_tag(history: list[Any]) -> bool:
    for msg in history:
        parts = getattr(msg, "parts", None)
        if not isinstance(parts, list):
            continue
        for part in parts:
            content = getattr(part, "content", None)
            if isinstance(content, str) and "<compaction-summary>" in content:
                return True
    return False


@pytest.mark.asyncio
async def test_compaction_pipeline_via_agent_runner_triggers_and_continues(
    tmp_state_dir: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """AgentRunner compacts long history and continues processing follow-up.

    Verifies:
    - Long message history triggers compaction (via token overflow threshold)
    - compaction emits a persistent log event ("compaction")
    - message history is reduced and contains <compaction-summary>
    - agent continues after compaction by processing queued follow-up message
    """

    _ = tmp_state_dir
    instance_id = "inst_compaction_integration"
    follow_up = "Please continue after compaction."

    # Create a long context bundle history (shared mode) so compaction has
    # meaningful old_messages to summarize.
    raw_tail: list[dict[str, str]] = []
    for i in range(60):
        raw_tail.append({"role": "user", "content": f"u{i}: " + ("x" * 200)})
        raw_tail.append({"role": "assistant", "content": f"a{i}: " + ("y" * 200)})

    bundle_path = tmp_state_dir / "context_bundle.json"
    bundle_path.write_text(
        json.dumps(
            {
                "bundle_id": "compaction_integration_bundle",
                "raw_tail": raw_tail,
            }
        )
    )

    persona = PersonaSpec(
        id="test-persona",
        description="A test persona.",
        prompt="You are a test agent.",
        model="test-model",
    )
    job = JobSpec(
        persona_ref="test@v1",
        instance_id=instance_id,
        max_turns=10,
        context_mode=ContextMode.shared,
        context_bundle_ref=str(bundle_path),
    )

    created = create_agent_state(instance_id, persona.id, job)
    assert isinstance(created, Success), f"create_agent_state failed: {created}"
    started = transition_state(instance_id, LifecycleEvent.start)
    assert isinstance(started, Success), f"transition_state(start) failed: {started}"

    state_result = read_state(instance_id)
    assert isinstance(state_result, Success)
    state = state_result.unwrap()
    assert state.state == AgentState.running

    # Configure compaction via startup config wiring path (AgentRunner.run).
    config_path = tmp_state_dir / "config.toml"
    config_path.write_text(
        """
[defaults]
context_window_tokens = 400

[compaction]
auto = true
prune = false
reserved = 10
""".strip()
    )
    monkeypatch.setenv("ANIMA_CONFIG", str(config_path))

    expected_context_window_tokens = 400
    observed_context_limits: list[int] = []
    compaction_results: list[dict[str, Any]] = []

    original_should_compact = agent_module.should_compact
    observed_compaction_decisions: list[bool] = []

    def _track_should_compact(
        token_usage: Any,
        compaction_config: Any,
        model_context_limit: int,
    ) -> bool:
        observed_context_limits.append(model_context_limit)
        # Simulate realistic last_request_prompt_tokens: the long context
        # bundle (120 messages × ~200 chars each) would produce a large prompt.
        # TestModel reports minimal usage, so we inject a realistic value
        # to trigger compaction as the test intends.
        if token_usage.last_request_prompt_tokens == 0:
            token_usage.last_request_prompt_tokens = 500  # > context_limit(400) - reserved(10) = 390
        compact = original_should_compact(
            token_usage,
            compaction_config,
            model_context_limit,
        )
        observed_compaction_decisions.append(compact)
        return compact

    monkeypatch.setattr(agent_module, "should_compact", _track_should_compact)

    original_run_compaction = agent_module.run_compaction

    async def _track_run_compaction(
        *,
        agent: Any,
        message_history: list[Any],
        compaction_config: Any,
        persona_compaction_prompt: Any,
        instance_id: str,
    ) -> Any:
        result = await original_run_compaction(
            agent=agent,
            message_history=list(message_history),
            compaction_config=compaction_config,
            persona_compaction_prompt=persona_compaction_prompt,
            instance_id=instance_id,
        )
        compacted_history = None
        if isinstance(result, Success):
            compacted_history = result.unwrap()
            compaction_result_history = [
                msg
                for msg in compacted_history
                if hasattr(msg, "parts") or hasattr(msg, "role")
            ]
        else:
            compaction_result_history = None

        compaction_results.append(
            {
                "input_len": len(message_history),
                "output_len": None
                if compaction_result_history is None
                else len(compaction_result_history),
                "history": compaction_result_history,
            }
        )
        return result

    monkeypatch.setattr(agent_module, "run_compaction", _track_run_compaction)

    class CapturingTestModel(TestModel):
        def __init__(self) -> None:
            super().__init__(custom_output_text="OK")
            self.requests: list[list[Any]] = []

        async def request(
            self,
            messages: list[Any],
            model_settings: Any,
            model_request_parameters: Any,
        ) -> Any:
            self.requests.append(list(messages))
            return await super().request(
                messages,
                model_settings,
                model_request_parameters,
            )

    class PreloadedFakeMCPServer(FakeMCPServer):
        last_instance: "PreloadedFakeMCPServer | None" = None

        def __init__(
            self,
            instance_id: str,
            agent_runner: object | None = None,
            interact_timeout_seconds: float = 30.0,
        ) -> None:
            super().__init__(instance_id, agent_runner, interact_timeout_seconds)
            PreloadedFakeMCPServer.last_instance = self
            self.queue.put_nowait(follow_up)

    monkeypatch.setattr("anima.shell.agent.AgentMCPServer", PreloadedFakeMCPServer)

    test_model = CapturingTestModel()

    # AgentRunner + TestModel (real model, no mocks)
    runner = AgentRunner(
        instance_id=instance_id,
        task="Do the task.",
        persona=persona,
        job=job,
        completion_policy=CompletionPolicy.auto,
        model_instance=test_model,
    )

    result = await runner.run()
    assert isinstance(result, Success), (
        f"AgentRunner run failed: {result.failure() if isinstance(result, Failure) else 'unknown'}"
    )

    assert runner._model_context_limit == expected_context_window_tokens
    assert observed_context_limits
    assert any(
        limit == expected_context_window_tokens for limit in observed_context_limits
    ), (
        f"Expected model_context_limit '{expected_context_window_tokens}' in should_compact calls, "
        f"got {observed_context_limits}"
    )
    assert any(decision is True for decision in observed_compaction_decisions), (
        "Compaction did not trigger based on resolved context limit despite long history. "
        "This indicates should_compact was not evaluated as expected."
    )

    fake_mcp = PreloadedFakeMCPServer.last_instance
    assert fake_mcp is not None

    # Prove the agent continued after compaction by processing the queued follow-up.
    assert fake_mcp.response is not None
    response_text, response_turn = fake_mcp.response
    assert "OK" in response_text
    assert response_turn >= 2

    # Verify compaction emitted a persistent log event and that it happened
    # before the follow-up was handled.
    log_result = read_log(instance_id)
    assert isinstance(log_result, Success)
    entries = log_result.unwrap()

    events = [e.get("event") for e in entries]
    assert "compaction" in events

    compaction_index = events.index("compaction")
    details = entries[compaction_index].get("details")
    assert isinstance(details, dict)
    assert details.get("old_count") is not None
    assert details.get("new_count") is not None
    assert int(details["old_count"]) > int(details["new_count"])
    assert int(details["old_count"]) >= 10

    # Verify compaction inserts <compaction-summary> into the compacted history.
    compacted_histories = [
        result["history"]
        for result in compaction_results
        if result["history"] is not None
    ]
    assert compacted_histories, "Expected compaction to return a compacted history"
    assert any(
        _has_compaction_summary_tag(history) for history in compacted_histories
    ), "Expected compaction to include <compaction-summary> tag"
