"""Integration tests for full-path runner seams (T4-T7).

These tests intentionally exercise behavior through fixture interfaces only:
- ``full_path_runner``
- ``mcp_socket_client``
"""

from __future__ import annotations

import asyncio
import json
from pathlib import Path
from typing import Any

import pytest
from mcp.types import TextContent
from returns.result import Failure, Success

from tests.integration_helpers import (
    full_path_runner,  # noqa: F401 - pytest fixture re-export
    mcp_socket_client,
    t7_runner,  # noqa: F401 - pytest fixture re-export
)


async def _wait_for_path(path: Path, timeout: float = 2.0) -> None:
    deadline = asyncio.get_running_loop().time() + timeout
    while asyncio.get_running_loop().time() < deadline:
        if path.exists():
            return
        await asyncio.sleep(0.02)
    raise AssertionError(f"Timed out waiting for path to exist: {path}")


async def _wait_for_path_absent(path: Path, timeout: float = 2.0) -> None:
    deadline = asyncio.get_running_loop().time() + timeout
    while asyncio.get_running_loop().time() < deadline:
        if not path.exists():
            return
        await asyncio.sleep(0.02)
    raise AssertionError(f"Timed out waiting for path cleanup: {path}")


def _read_state(state_dir: Path, instance_id: str) -> dict[str, Any]:
    state_file = state_dir / "agents" / instance_id / "state.json"
    assert state_file.exists(), f"Missing state.json: {state_file}"
    payload = json.loads(state_file.read_text())
    assert isinstance(payload, dict), f"state.json must be object, got: {payload!r}"
    return payload


@pytest.mark.asyncio
async def test_t4_mcp_socket_created_and_cleaned_up(full_path_runner) -> None:  # noqa: F811
    """(T4) MCP socket is created during run and cleaned up after."""
    runner = full_path_runner["runner"]
    socket_path: Path = full_path_runner["socket_path"]

    run_task = asyncio.create_task(runner.run())
    await _wait_for_path(socket_path, timeout=3.0)

    result = await run_task
    assert isinstance(result, Success), (
        f"T4 setup failed: run() returned Failure: {result.failure() if isinstance(result, Failure) else result}"
    )

    await _wait_for_path_absent(socket_path, timeout=3.0)


@pytest.mark.asyncio
async def test_t5_state_transitions_pending_running_completed(full_path_runner) -> None:  # noqa: F811
    """(T5) State progresses pending -> running -> completed over full path."""
    runner = full_path_runner["runner"]
    state_dir: Path = full_path_runner["state_dir"]
    instance_id: str = full_path_runner["instance_id"]

    before = _read_state(state_dir, instance_id)
    assert before.get("state") == "running", (
        f"T5 expected pre-run state='running' (after fixture start), got {before.get('state')!r}"
    )

    result = await runner.run()
    assert isinstance(result, Success), (
        f"T5 setup failed: run() returned Failure: {result.failure() if isinstance(result, Failure) else result}"
    )

    after = _read_state(state_dir, instance_id)
    assert after.get("state") == "completed", (
        f"T5 expected final state='completed', got {after.get('state')!r}"
    )

    log_file = state_dir / "agents" / instance_id / "log.jsonl"
    assert log_file.exists(), f"T5 expected lifecycle log file: {log_file}"
    log_text = log_file.read_text()
    assert "pending" in log_text, "T5 expected lifecycle evidence for pending state"
    assert "running" in log_text, "T5 expected lifecycle evidence for running state"
    assert "completed" in log_text, "T5 expected lifecycle evidence for completed state"


@pytest.mark.asyncio
async def test_t6_result_has_response_text_and_token_usage(full_path_runner) -> None:  # noqa: F811
    """(T6) Full-path run returns response text and positive token usage."""
    runner = full_path_runner["runner"]
    expected_text: str = full_path_runner["test_response"]

    result = await runner.run()
    assert isinstance(result, Success), (
        f"T6 expected Success from run(), got {result.failure() if isinstance(result, Failure) else result}"
    )

    agent_result = result.unwrap()
    assert agent_result.response_text, "T6 expected non-empty response_text"
    assert expected_text in agent_result.response_text, (
        f"T6 expected response_text to include {expected_text!r}, got {agent_result.response_text!r}"
    )

    assert agent_result.token_usage is not None, "T6 expected token_usage to be present"
    assert agent_result.token_usage.total_tokens > 0, (
        f"T6 expected total_tokens > 0, got {agent_result.token_usage.total_tokens}"
    )


@pytest.mark.asyncio
async def test_t6_response_logged_to_jsonl(full_path_runner) -> None:  # noqa: F811
    """(T6.1) Response text is logged to log.jsonl with turn_number."""
    runner = full_path_runner["runner"]
    state_dir: Path = full_path_runner["state_dir"]
    instance_id: str = full_path_runner["instance_id"]
    expected_text: str = full_path_runner["test_response"]

    result = await runner.run()
    assert isinstance(result, Success), (
        f"T6.1 expected Success from run(), got {result.failure() if isinstance(result, Failure) else result}"
    )

    log_file = state_dir / "agents" / instance_id / "log.jsonl"
    assert log_file.exists(), f"T6.1 expected log.jsonl file: {log_file}"

    log_lines = log_file.read_text().strip().split("\n")
    log_entries = [json.loads(line) for line in log_lines]

    # Find the 'response' event
    response_events = [e for e in log_entries if e.get("event") == "response"]
    assert len(response_events) >= 1, (
        f"T6.1 expected at least one 'response' event in log, found events: {[e.get('event') for e in log_entries]}"
    )

    response_entry = response_events[0]
    assert "response_text" in response_entry.get("details", {}), (
        f"T6.1 expected 'response_text' in response event details, got: {response_entry}"
    )
    assert expected_text in response_entry["details"]["response_text"], (
        f"T6.1 expected response_text to contain {expected_text!r}, "
        f"got: {response_entry['details']['response_text']!r}"
    )
    assert "turn_number" in response_entry.get("details", {}), (
        f"T6.1 expected 'turn_number' in response event details, got: {response_entry}"
    )
    assert isinstance(response_entry["details"]["turn_number"], int), (
        f"T6.1 expected turn_number to be int, got: {type(response_entry['details']['turn_number'])}"
    )


@pytest.mark.asyncio
async def test_t6_spawn_task_replayed_as_single_user_message(full_path_runner) -> None:  # noqa: F811
    """(T6.2) Initial spawn task logs exactly one user_message for replay."""
    from anima.core.talk_history import (
        filter_conversation_events,
        format_conversation_turns,
    )

    runner = full_path_runner["runner"]
    state_dir: Path = full_path_runner["state_dir"]
    instance_id: str = full_path_runner["instance_id"]
    spawn_task: str = runner.task

    result = await runner.run()
    assert isinstance(result, Success), (
        "T6.2 expected Success from run(), "
        f"got {result.failure() if isinstance(result, Failure) else result}"
    )

    log_file = state_dir / "agents" / instance_id / "log.jsonl"
    assert log_file.exists(), f"T6.2 expected log.jsonl file: {log_file}"
    log_entries = [
        json.loads(line) for line in log_file.read_text().strip().split("\n")
    ]

    user_events = [
        entry
        for entry in log_entries
        if entry.get("event") == "user_message"
        and entry.get("details", {}).get("message_text") == spawn_task
    ]
    assert len(user_events) == 1, (
        f"T6.2 expected exactly one spawn user_message, got {len(user_events)}"
    )

    turns = format_conversation_turns(filter_conversation_events(log_entries))
    replayed_spawn_turns = [
        turn for turn in turns if turn[0] == "user" and turn[2] == spawn_task
    ]
    assert len(replayed_spawn_turns) == 1, (
        f"T6.2 expected one replayed spawn user turn, got {len(replayed_spawn_turns)}"
    )
    assert replayed_spawn_turns[0][1] == 1, (
        f"T6.2 expected spawn turn_number=1, got {replayed_spawn_turns[0][1]}"
    )


@pytest.mark.asyncio
async def test_t7_anima_status_queryable_during_run(t7_runner) -> None:  # noqa: F811
    """(T7) MCP ``anima.status`` is queryable while run is in progress.

    Uses daemon lifecycle + explicit completion policy so the agent stays running
    during the status query, providing a stable window for MCP handshake.
    """
    runner = t7_runner["runner"]
    socket_path: Path = t7_runner["socket_path"]
    instance_id: str = t7_runner["instance_id"]

    run_task = asyncio.create_task(runner.run())

    try:
        # Wait for socket to appear
        await _wait_for_path(socket_path, timeout=3.0)

        # Connect to the running agent's MCP server
        async with mcp_socket_client(socket_path) as client:
            session = client["session"]
            await session.initialize()
            call_result = await session.call_tool("anima.status", arguments={})

        assert call_result.content, "T7 expected anima.status content"
        first_block = call_result.content[0]
        assert isinstance(first_block, TextContent), (
            f"T7 expected TextContent response, got {type(first_block).__name__}"
        )
        status_payload = json.loads(first_block.text)

        # Status should show running (daemon stays running until explicit finish)
        assert status_payload.get("instance_id") == instance_id, (
            f"T7 expected instance_id={instance_id!r}, got {status_payload.get('instance_id')!r}"
        )
        assert status_payload.get("state") == "running", (
            f"T7 expected state='running', got {status_payload.get('state')!r}"
        )

    finally:
        # Daemon agent runs forever in interact loop, cancel to clean up
        run_task.cancel()
        try:
            await run_task
        except asyncio.CancelledError:
            pass
