"""Integration test fixtures for AgentRunner and MCP testing.

Provides shared fixtures for integration tests:
1. env_test_runner: AgentRunner + AgentStateData with env configured
2. full_path_runner: AgentRunner + TestModel + short socket path
3. mcp_socket_client: Connect to MCP socket and call anima.status()

Spec source: vectl step test-scaffolding
"""

import os
import uuid
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, AsyncIterator

import pytest
from returns.result import Success

from anima.core.schemas import (
    AgentState,
    AgentStateData,
    CompletionPolicy,
    JobSpec,
    LifecycleType,
    PersonaSpec,
    TokenUsage,
)
from anima.core.lifecycle import LifecycleEvent
from anima.shell.agent import AgentRunner
from anima.shell.agent_lifecycle import configure_run_environment
from anima.shell.state import create_agent_state, transition_state


# =============================================================================
# Fixture 1: env_test_runner
# =============================================================================


@pytest.fixture
def env_test_runner(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
    """Construct AgentRunner + AgentStateData, configure env, return snapshot.

    Creates an AgentRunner with minimal configuration, creates AgentStateData,
    calls configure_run_environment(), and returns environment snapshot.

    Args:
        tmp_path: Pytest temporary path fixture.
        monkeypatch: Pytest monkeypatch fixture for env isolation.

    Returns:
        Dict with:
            - runner: AgentRunner instance
            - state: AgentStateData instance
            - env_snapshot: Dict of captured environment variables

    Usage:
        # In a test:
        # result = env_test_runner
        # runner = result["runner"]
        # state = result["state"]
        # env = result["env_snapshot"]
        # assert "ANIMA_AGENT_ID" in env
        # assert env["ANIMA_AGENT_ID"] == state.instance_id
    """
    # Create isolated state directory or use existing ANIMA_STATE
    # T2: Preserve pre-existing ANIMA_STATE if already set by test
    existing_state = os.environ.get("ANIMA_STATE")
    if existing_state:
        state_dir = Path(existing_state)
    else:
        state_dir = tmp_path / ".anima"
        state_dir.mkdir(parents=True, exist_ok=True)
        monkeypatch.setenv("ANIMA_STATE", str(state_dir))

    # Create minimal AgentRunner
    instance_id = f"inst_env_{uuid.uuid4().hex[:8]}"
    runner = AgentRunner(
        instance_id=instance_id,
        task="Test environment configuration",
        persona=PersonaSpec(
            id="test-persona", description="A test persona.",
            prompt="Test prompt",
            model="test-model",
        ),
        job=JobSpec(
            persona_ref="test@v1",
            instance_id=instance_id,
        ),
        completion_policy=CompletionPolicy.auto,
    )

    # Create AgentStateData
    now = "2024-01-01T00:00:00Z"
    state = AgentStateData(
        instance_id=instance_id,
        trace_id=f"tr_{uuid.uuid4().hex[:8]}",
        persona_id="test-persona",
        state=AgentState.running,
        lifecycle=LifecycleType.ephemeral,
        created_at=now,
        updated_at=now,
        token_usage=TokenUsage(),
    )

    # Configure run environment
    configure_run_environment(instance_id, state, None)

    # Capture environment snapshot
    env_snapshot = {
        "ANIMA_AGENT_ID": os.environ.get("ANIMA_AGENT_ID"),
        "ANIMA_STATE": os.environ.get("ANIMA_STATE"),
        "ANIMA_TRACE_ID": os.environ.get("ANIMA_TRACE_ID"),
        "ANIMA_CAPABILITY_TOKEN_JSON": os.environ.get("ANIMA_CAPABILITY_TOKEN_JSON"),
    }

    return {
        "runner": runner,
        "state": state,
        "env_snapshot": env_snapshot,
    }


# =============================================================================
# Fixture 2: full_path_runner
# =============================================================================


@pytest.fixture
async def full_path_runner(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> AsyncIterator[dict[str, Any]]:
    """AgentRunner + TestModel with short socket path for macOS.

    Creates a state directory in /tmp to ensure socket path stays under
    macOS 108-byte Unix socket path limit. Sets up AgentRunner with TestModel
    and initializes state, ready to call run().

    Uses ephemeral lifecycle + auto completion for fast tests (T4-T6).
    For T7 (status query during run), use t7_runner fixture instead.

    macOS socket path limit: 108 bytes including null terminator
    Path: /tmp/anima_test_<uuid8>/agents/<instance_id>/mcp.sock
    Example: /tmp/anima_test_a1b2c3d4/agents/inst_12345678/mcp.sock (~60 bytes)

    Args:
        tmp_path: Pytest temporary path (not used, override with short_path).
        monkeypatch: Pytest monkeypatch for env isolation.

    Yields:
        Dict with:
            - runner: AgentRunner instance (TestModel configured)
            - state_dir: Short Path to state directory
            - socket_path: Expected socket path
            - instance_id: Agent instance ID
            - test_response: Expected response text from TestModel

    Usage:
        # In a test:
        # result = await full_path_runner
        # runner = result["runner"]
        # socket_path = result["socket_path"]
        # # Socket path is short enough for macOS
        # assert len(str(socket_path)) < 100
    """
    # Use /tmp for short paths (macOS has 108-byte limit)

    short_state_dir = Path(f"/tmp/anima_test_{uuid.uuid4().hex[:8]}")
    short_state_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setenv("ANIMA_STATE", str(short_state_dir))

    # Create agent state
    instance_id = f"inst_{uuid.uuid4().hex[:8]}"
    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=5,
    )

    # Create state file
    state_result = create_agent_state(instance_id, "test-persona", job)
    assert isinstance(state_result, Success), (
        f"Failed to create state: {state_result.failure()}"
    )

    # Create TestModel with custom output response
    # Spec source: integration-test-seams.test-full-path-run-fix
    # Pass the TestModel instance directly via model_instance parameter.
    # This bypasses model resolution and allows custom test output.
    test_response = "Test response from agent."
    from pydantic_ai.models.test import TestModel

    # Create AgentRunner with TestModel instance
    runner = AgentRunner(
        instance_id=instance_id,
        task="Test task with short socket path",
        persona=persona,
        job=job,
        completion_policy=CompletionPolicy.auto,
        model_instance=TestModel(custom_output_text=test_response),
    )

    # Transition to running state
    transition_state(instance_id, LifecycleEvent.start)
    # Note: start event transitions pending -> running

    # NOTE: DO NOT inject FakeMCPServer here - the runner needs to create
    # a real AgentMCPServer for T4/T7 tests that need actual socket connectivity.
    # The runner.run() method will create AgentMCPServer at line 621.

    # Calculate socket path (for verification)
    socket_path = short_state_dir / "agents" / instance_id / "mcp.sock"

    yield {
        "runner": runner,
        "state_dir": short_state_dir,
        "socket_path": socket_path,
        "instance_id": instance_id,
        "test_response": test_response,
    }

    # Cleanup
    import shutil

    if short_state_dir.exists():
        shutil.rmtree(short_state_dir, ignore_errors=True)


# =============================================================================
# Fixture 2b: t7_runner (daemon lifecycle for status query tests)
# =============================================================================


@pytest.fixture
async def t7_runner(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> AsyncIterator[dict[str, Any]]:
    """AgentRunner for T7 status query test with daemon lifecycle.

    Uses daemon lifecycle + explicit completion so agent stays running
    during the status query test. This fixture is specifically for T7
    which needs to query anima.status() while the agent is running.

    Args:
        tmp_path: Pytest temporary path (not used, override with short_path).
        monkeypatch: Pytest monkeypatch for env isolation.

    Yields:
        Dict with:
            - runner: AgentRunner instance (TestModel + daemon lifecycle)
            - state_dir: Short Path to state directory
            - socket_path: Expected socket path
            - instance_id: Agent instance ID
    """
    # Use /tmp for short paths (macOS has 108-byte limit)
    short_state_dir = Path(f"/tmp/anima_test_{uuid.uuid4().hex[:8]}")
    short_state_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setenv("ANIMA_STATE", str(short_state_dir))

    # Create agent state with DAEMON lifecycle (stays running)
    instance_id = f"inst_{uuid.uuid4().hex[:8]}"
    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=5,
        lifecycle=LifecycleType.daemon,  # Daemon stays running
    )

    # Create state file
    state_result = create_agent_state(instance_id, "test-persona", job)
    assert isinstance(state_result, Success), (
        f"Failed to create state: {state_result.failure()}"
    )

    # Create AgentRunner with EXPLICIT completion (no auto-complete)
    from pydantic_ai.models.test import TestModel

    runner = AgentRunner(
        instance_id=instance_id,
        task="Test task for T7 status query",
        persona=persona,
        job=job,
        completion_policy=CompletionPolicy.explicit,  # Never auto-complete
        model_instance=TestModel(custom_output_text="Test response", call_tools=[]),
    )

    # Note: DO NOT transition to running here - let runner.run() do it
    # This ensures MCP server is created by runner.run()

    # Calculate socket path
    socket_path = short_state_dir / "agents" / instance_id / "mcp.sock"

    yield {
        "runner": runner,
        "state_dir": short_state_dir,
        "socket_path": socket_path,
        "instance_id": instance_id,
    }

    # Cleanup
    import shutil

    if short_state_dir.exists():
        shutil.rmtree(short_state_dir, ignore_errors=True)


# =============================================================================
# Fixture 3: mcp_socket_client
# =============================================================================


@asynccontextmanager
async def mcp_socket_client(
    socket_path: Path, timeout: float = 5.0
) -> AsyncIterator[dict[str, Any]]:
    """Connect to MCP Unix socket and call anima.status().

    This is a helper function (not a fixture) that can be used in tests
    to verify MCP server connectivity.

    Uses httpx with Unix socket transport to connect to the MCP server
    running on a Unix domain socket.

    Args:
        socket_path: Path to the Unix domain socket.
        timeout: Connection timeout in seconds.

    Yields:
        Dict with:
            - session: MCP ClientSession
            - status_result: Result from anima.status() call (or None if not called)

    Usage:
        # In a test:
        # async with mcp_socket_client(socket_path) as client:
        #     result = await call_status_tool(client["session"])
        #     assert "instance_id" in result
        # # Or with helper:
        # result = await call_anima_status(socket_path)
        # assert result["state"] == "running"

    Raises:
        FileNotFoundError: If socket_path doesn't exist.
        ConnectionError: If unable to connect to socket.
        TimeoutError: If connection times out.
    """
    from mcp import ClientSession
    from mcp.client.streamable_http import streamable_http_client
    import httpx

    if not socket_path.exists():
        raise FileNotFoundError(f"Socket not found: {socket_path}")

    # Create httpx client configured for Unix socket
    # The URL is "http://localhost" but the transport connects via Unix socket
    # Spec source: integration-test-seams.test-full-path-run-fix
    # httpx supports Unix domain sockets via uds parameter on AsyncHTTPTransport
    client = httpx.AsyncClient(
        transport=httpx.AsyncHTTPTransport(uds=str(socket_path)),
        timeout=httpx.Timeout(timeout, read=timeout * 2),
    )

    try:
        # Connect to MCP server via Unix socket
        # URL uses http://localhost/mcp (default streamable-http path)
        async with streamable_http_client(
            "http://localhost/mcp", http_client=client
        ) as (
            read_stream,
            write_stream,
            _get_session_id,
        ):
            async with ClientSession(read_stream, write_stream) as session:
                yield {
                    "session": session,
                    "status_result": None,  # Populated by helper below
                }
    finally:
        await client.aclose()


async def call_anima_status(
    socket_path: Path,
    timeout: float = 5.0,
) -> dict[str, Any]:
    """Connect to MCP socket and call anima.status() tool.

    Convenience helper that wraps mcp_socket_client and calls the
    status tool, returning the result.

    Args:
        socket_path: Path to the Unix domain socket.
        timeout: Connection timeout in seconds.

    Returns:
        Dict with agent status fields per docs/INTERFACES.md Section B:
            - instance_id: Agent instance ID
            - state: Agent state (pending/running/completed/failed/killed)
            - persona_name: Name of the persona
            - lifecycle: ephemeral or persistent
            - token_usage: Token usage metrics
            - etc.

    Raises:
        FileNotFoundError: If socket not found.
        ConnectionError: If connection fails.
        TimeoutError: If timeout.
    """
    from mcp import ClientSession
    from mcp.client.streamable_http import streamable_http_client
    import httpx

    if not socket_path.exists():
        raise FileNotFoundError(f"Socket not found: {socket_path}")

    # Create httpx client configured for Unix socket
    client = httpx.AsyncClient(
        transport=httpx.AsyncHTTPTransport(uds=str(socket_path)),
        timeout=httpx.Timeout(timeout, read=timeout * 2),
    )

    try:
        async with streamable_http_client(
            "http://localhost/mcp", http_client=client
        ) as (
            read_stream,
            write_stream,
            _get_session_id,
        ):
            async with ClientSession(read_stream, write_stream) as session:
                # Initialize MCP session
                await session.initialize()

                # Call anima.status() tool
                result = await session.call_tool("anima.status", arguments={})

                # Extract content from result
                # result.content is a list of content blocks
                if result.content and len(result.content) > 0:
                    content = result.content[0]
                    # Check if it's TextContent via isinstance
                    from mcp.types import TextContent

                    if isinstance(content, TextContent):
                        import json

                        return json.loads(content.text)

                return {"error": "No content in response"}
    finally:
        await client.aclose()


# =============================================================================
# Helper: Verify socket path length
# =============================================================================


def verify_socket_path_length(socket_path: Path) -> bool:
    """Verify socket path is under macOS 108-byte limit.

    macOS limits Unix socket paths to 108 bytes including null terminator.
    This is a hard limit from struct sockaddr_un.sun_path.

    Args:
        socket_path: Path to check.

    Returns:
        True if path is under limit, False otherwise.

    Usage:
        # In a test:
        # short_path = Path("/tmp/test/mcp.sock")
        # assert verify_socket_path_length(short_path)
    """
    # Convert to bytes and check length
    path_bytes = str(socket_path).encode("utf-8")
    # macOS limit: 108 bytes including null terminator
    # Use 104 to be safe (leave room for null + potential locale differences)
    return len(path_bytes) < 104


# =============================================================================
# Exports
# =============================================================================

__all__ = [
    "env_test_runner",
    "full_path_runner",
    "t7_runner",
    "mcp_socket_client",
    "call_anima_status",
    "verify_socket_path_length",
]
