"""Tests for Codex MCP adapter."""

import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from zwarm.adapters.codex_mcp import CodexMCPAdapter, MCPClient
from zwarm.core.models import SessionMode, SessionStatus


class TestMCPClient:
    """Tests for the MCP client."""

    def test_next_id_increments(self):
        """Test that request IDs increment properly."""
        client = MCPClient()
        assert client._next_id() == 1
        assert client._next_id() == 2
        assert client._next_id() == 3


class TestCodexMCPAdapter:
    """Tests for the Codex MCP adapter."""

    @pytest.fixture
    def adapter(self):
        return CodexMCPAdapter()

    @pytest.mark.asyncio
    async def test_start_session_creates_session(self, adapter):
        """Test that start_session creates a proper session object."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Mock the _call_codex method (now synchronous)
            with patch.object(adapter, "_call_codex", return_value={
                "conversation_id": "conv-123",
                "response": "Hello! I'll help you with that.",
                "raw_messages": [],
                "usage": {},
                "total_usage": {},
            }):
                session = await adapter.start_session(
                    task="Say hello",
                    working_dir=Path(tmpdir),
                    mode="sync",
                )

                assert session.adapter == "codex_mcp"
                assert session.mode == SessionMode.SYNC
                assert session.status == SessionStatus.ACTIVE
                assert session.conversation_id == "conv-123"
                assert len(session.messages) == 2
                assert session.messages[0].role == "user"
                assert session.messages[1].role == "assistant"

    @pytest.mark.asyncio
    async def test_send_message_continues_conversation(self, adapter):
        """Test that send_message continues an existing conversation."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Mock _call_codex for start_session
            with patch.object(adapter, "_call_codex", return_value={
                "conversation_id": "conv-123",
                "response": "Initial response",
                "raw_messages": [],
                "usage": {},
                "total_usage": {},
            }):
                session = await adapter.start_session(
                    task="Start task",
                    working_dir=Path(tmpdir),
                    mode="sync",
                )

            # Mock _call_codex_reply for send_message
            with patch.object(adapter, "_call_codex_reply", return_value={
                "response": "Follow-up response",
                "raw_messages": [],
                "usage": {},
                "total_usage": {},
            }):
                response = await adapter.send_message(session, "Continue please")

                assert response == "Follow-up response"
                assert len(session.messages) == 4  # 2 from start + 2 from reply

    @pytest.mark.asyncio
    async def test_send_message_fails_on_async_session(self, adapter):
        """Test that send_message raises error for async sessions."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create async session (mocked to avoid actually starting codex)
            with patch("subprocess.Popen") as mock_popen:
                mock_popen.return_value = MagicMock()
                session = await adapter.start_session(
                    task="Async task",
                    working_dir=Path(tmpdir),
                    mode="async",
                )

            with pytest.raises(ValueError, match="Cannot send message to async session"):
                await adapter.send_message(session, "Should fail")

    @pytest.mark.asyncio
    async def test_check_status_async_running(self, adapter):
        """Test checking status of a running async session."""
        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("subprocess.Popen") as mock_popen:
                mock_proc = MagicMock()
                mock_proc.poll.return_value = None  # Still running
                mock_popen.return_value = mock_proc

                session = await adapter.start_session(
                    task="Async task",
                    working_dir=Path(tmpdir),
                    mode="async",
                )

                status = await adapter.check_status(session)
                assert status["status"] == "running"

    @pytest.mark.asyncio
    async def test_check_status_async_completed(self, adapter):
        """Test checking status of a completed async session."""
        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("subprocess.Popen") as mock_popen:
                mock_proc = MagicMock()
                mock_proc.poll.return_value = 0  # Completed
                mock_proc.communicate.return_value = ("Output text", "")
                mock_popen.return_value = mock_proc

                session = await adapter.start_session(
                    task="Async task",
                    working_dir=Path(tmpdir),
                    mode="async",
                )

                status = await adapter.check_status(session)
                assert status["status"] == "completed"
                assert session.status == SessionStatus.COMPLETED

    @pytest.mark.asyncio
    async def test_stop_session(self, adapter):
        """Test stopping a session."""
        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("subprocess.Popen") as mock_popen:
                mock_proc = MagicMock()
                mock_proc.poll.return_value = None  # Running
                mock_popen.return_value = mock_proc

                session = await adapter.start_session(
                    task="Async task",
                    working_dir=Path(tmpdir),
                    mode="async",
                )

                await adapter.stop(session)

                mock_proc.terminate.assert_called_once()
                assert session.status == SessionStatus.FAILED

    def test_extract_response_content_list(self, adapter):
        """Test response extraction from content list."""
        result = {"content": [{"text": "Line 1"}, {"text": "Line 2"}]}
        response = adapter._extract_response(result)
        assert response == "Line 1\nLine 2"

    def test_extract_response_output(self, adapter):
        """Test response extraction from output field."""
        result = {"output": "Direct output"}
        response = adapter._extract_response(result)
        assert response == "Direct output"

    def test_extract_response_fallback(self, adapter):
        """Test response extraction fallback to JSON."""
        result = {"unknown": "field"}
        response = adapter._extract_response(result)
        assert "unknown" in response

    def test_parse_jsonl_output(self, adapter):
        """Test parsing JSONL output from codex exec --json."""
        jsonl_output = """{"type":"thread.started","thread_id":"abc123"}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"Thinking..."}}
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"The answer is 4"}}
{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":10}}"""

        parsed = adapter._parse_jsonl_output(jsonl_output)

        assert parsed["response"] == "The answer is 4"
        assert parsed["thread_id"] == "abc123"
        assert parsed["usage"]["input_tokens"] == 100
        assert parsed["usage"]["output_tokens"] == 10
        assert len(parsed["events"]) == 5

    def test_parse_jsonl_output_multiple_messages(self, adapter):
        """Test parsing JSONL with multiple agent messages."""
        jsonl_output = """{"type":"thread.started","thread_id":"xyz"}
{"type":"item.completed","item":{"type":"agent_message","text":"First part"}}
{"type":"item.completed","item":{"type":"agent_message","text":"Second part"}}
{"type":"turn.completed","usage":{"input_tokens":50,"output_tokens":20}}"""

        parsed = adapter._parse_jsonl_output(jsonl_output)

        assert parsed["response"] == "First part\nSecond part"
        assert parsed["thread_id"] == "xyz"

    def test_parse_jsonl_output_empty(self, adapter):
        """Test parsing empty JSONL output."""
        parsed = adapter._parse_jsonl_output("")
        assert parsed["response"] == ""
        assert parsed["usage"] == {}
        assert parsed["thread_id"] is None

    def test_parse_jsonl_output_malformed_lines(self, adapter):
        """Test parsing JSONL with some malformed lines."""
        jsonl_output = """{"type":"thread.started","thread_id":"test123"}
not valid json
{"type":"item.completed","item":{"type":"agent_message","text":"Valid response"}}
also not json
{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":5}}"""

        parsed = adapter._parse_jsonl_output(jsonl_output)

        # Should still extract valid data
        assert parsed["response"] == "Valid response"
        assert parsed["thread_id"] == "test123"
        assert len(parsed["events"]) == 3  # Only valid JSON lines


@pytest.mark.integration
class TestCodexMCPIntegration:
    """
    Integration tests that run against real codex mcp-server.

    These tests are skipped by default. Run with:
    pytest -m integration
    """

    @pytest.fixture
    def adapter(self):
        return CodexMCPAdapter()

    @pytest.mark.asyncio
    async def test_real_sync_conversation(self, adapter):
        """Test a real sync conversation with codex."""
        with tempfile.TemporaryDirectory() as tmpdir:
            try:
                # Start a simple session
                session = await adapter.start_session(
                    task="What is 2 + 2? Reply with just the number.",
                    working_dir=Path(tmpdir),
                    mode="sync",
                    sandbox="read-only",
                )

                assert session.conversation_id is not None
                assert len(session.messages) >= 2

                # Continue conversation
                response = await adapter.send_message(
                    session,
                    "And what is that number times 3?"
                )

                assert response is not None
                assert len(session.messages) >= 4

            finally:
                await adapter.cleanup()


if __name__ == "__main__":
    pytest.main([__file__, "-v", "-m", "not integration"])
