"""Comprehensive test suite for claif_gem transport layer."""

from unittest.mock import AsyncMock, Mock, patch

import pytest
from claif.common import TransportError
from claif.common.types import TextBlock
from tenacity import RetryError

from claif_gem.transport import GeminiTransport
from claif_gem.types import GeminiMessage, GeminiOptions, ResultMessage


class TestGeminiTransport:
    """Test suite for GeminiTransport."""

    @pytest.fixture
    def transport(self):
        """Create a transport instance."""
        return GeminiTransport()

    @pytest.fixture
    def mock_subprocess(self):
        """Mock asyncio subprocess."""
        with patch("asyncio.create_subprocess_exec") as mock_create:
            yield mock_create

    def test_init(self):
        """Test transport initialization."""
        transport = GeminiTransport()
        assert transport.process is None
        assert transport.session_id is not None
        assert isinstance(transport.session_id, str)
        # UUID format check
        assert len(transport.session_id) == 36  # Standard UUID with dashes

    @pytest.mark.asyncio
    async def test_connect(self, transport):
        """Test connect is a no-op."""
        await transport.connect()  # Should not raise

    @pytest.mark.asyncio
    async def test_disconnect_no_process(self, transport):
        """Test disconnect when no process is running."""
        await transport.disconnect()  # Should not raise
        assert transport.process is None

    @pytest.mark.asyncio
    async def test_disconnect_with_process(self, transport):
        """Test disconnect with running process."""
        mock_process = Mock()
        mock_process.terminate = Mock()
        mock_process.wait = AsyncMock()

        transport.process = mock_process
        await transport.disconnect()

        mock_process.terminate.assert_called_once()
        mock_process.wait.assert_called_once()
        assert transport.process is None

    @pytest.mark.asyncio
    async def test_disconnect_with_error(self, transport):
        """Test disconnect when process termination fails."""
        mock_process = Mock()
        mock_process.terminate = Mock(side_effect=Exception("Termination error"))

        transport.process = mock_process

        # Should not raise, just log
        await transport.disconnect()
        assert transport.process is None

    def test_build_command_minimal(self, transport):
        """Test command building with minimal options."""
        with patch("claif_gem.transport.find_executable", return_value="gemini"):
            options = GeminiOptions()
            command = transport._build_command("Hello", options)

            assert command == ["gemini", "-y", "-p", "Hello"]

    def test_build_command_with_all_options(self, transport):
        """Test command building with all options."""
        with patch("claif_gem.transport.find_executable", return_value="/usr/bin/gemini"):
            options = GeminiOptions(
                model="gemini-pro",
                temperature=0.7,
                system_prompt="You are helpful",
                max_context_length=2048,
                images=["/img1.png", "/img2.jpg"],
                verbose=True,
                auto_approve=True,
                yes_mode=True,
            )
            command = transport._build_command("Test prompt", options)

            expected = [
                "/usr/bin/gemini",
                "-y",
                "-d",  # verbose mode
                "-m",
                "gemini-pro",
                "-t",
                "0.7",
                "-s",
                "You are helpful",
                "--max-context",
                "2048",
                "-p",
                "Test prompt",
                "-i",
                "/img1.png",
                "-i",
                "/img2.jpg",
            ]
            assert command == expected

    def test_build_command_no_auto_approve(self, transport):
        """Test command without auto-approve."""
        with patch("claif_gem.transport.find_executable", return_value="gemini"):
            options = GeminiOptions(auto_approve=False, yes_mode=False)
            command = transport._build_command("Test", options)

            # Should not include -y flag
            assert command == ["gemini", "-p", "Test"]

    def test_build_command_with_shell_script(self, transport):
        """Test command with script path (e.g., 'deno run script.js')."""
        with patch("claif_gem.transport.find_executable", return_value="deno run /path/to/gemini.js"):
            with patch("claif_gem.transport.Path.exists", return_value=False):
                options = GeminiOptions()
                command = transport._build_command("Test", options)

                assert command[:3] == ["deno", "run", "/path/to/gemini.js"]
                assert "-y" in command
                assert "-p" in command
                assert "Test" in command

    def test_build_env(self, transport):
        """Test environment variable building."""
        with patch("claif_gem.transport.inject_claif_bin_to_path") as mock_inject:
            mock_inject.return_value = {"PATH": "/custom/bin:/usr/bin"}

            env = transport._build_env()

            assert env["GEMINI_CLI_INTERNAL"] == "1"
            assert env["CLAIF_PROVIDER"] == "gemini"
            assert env["PATH"] == "/custom/bin:/usr/bin"

    def test_build_env_import_error(self, transport):
        """Test environment building when claif import fails."""
        with patch("claif_gem.transport.inject_claif_bin_to_path", side_effect=ImportError):
            with patch("os.environ.copy", return_value={"PATH": "/bin"}):
                env = transport._build_env()

                assert env["GEMINI_CLI_INTERNAL"] == "1"
                assert env["CLAIF_PROVIDER"] == "gemini"
                assert env["PATH"] == "/bin"

    def test_find_cli_success(self, transport):
        """Test CLI finding success."""
        with patch("claif_gem.transport.find_executable", return_value="/opt/gemini/bin/gemini"):
            result = transport._find_cli()
            assert result == "/opt/gemini/bin/gemini"

    def test_find_cli_with_exec_path(self, transport):
        """Test CLI finding with explicit exec_path."""
        with patch("claif_gem.transport.find_executable", return_value="/custom/gemini"):
            result = transport._find_cli("/custom/gemini")
            assert result == "/custom/gemini"

    def test_find_cli_error(self, transport):
        """Test CLI finding error."""
        with patch("claif_gem.transport.find_executable", side_effect=TransportError("Not found")):
            with pytest.raises(TransportError, match="Not found"):
                transport._find_cli()

    @pytest.mark.asyncio
    async def test_execute_query_success_json(self, transport, mock_subprocess):
        """Test successful query execution with JSON response."""
        # Mock process
        mock_process = Mock()
        mock_process.returncode = 0
        mock_process.communicate = AsyncMock(
            return_value=(b'{"content": "Hello from Gemini", "role": "assistant"}', b"")
        )
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        messages = []
        async for msg in transport._execute_query(command, env, options, "Test"):
            messages.append(msg)

        assert len(messages) == 2
        assert isinstance(messages[0], GeminiMessage)
        assert len(messages[0].content) == 1
        assert messages[0].content[0].text == "Hello from Gemini"
        assert messages[0].role == "assistant"
        assert isinstance(messages[1], ResultMessage)
        assert messages[1].error is False

    @pytest.mark.asyncio
    async def test_execute_query_success_plain_text(self, transport, mock_subprocess):
        """Test successful query with plain text response."""
        mock_process = Mock()
        mock_process.returncode = 0
        mock_process.communicate = AsyncMock(return_value=(b"Plain text response from Gemini", b""))
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        messages = []
        async for msg in transport._execute_query(command, env, options, "Test"):
            messages.append(msg)

        assert len(messages) == 2
        assert isinstance(messages[0], GeminiMessage)
        assert len(messages[0].content) == 1
        assert messages[0].content[0].text == "Plain text response from Gemini"
        assert isinstance(messages[1], ResultMessage)

    @pytest.mark.asyncio
    async def test_execute_query_error_non_retryable(self, transport, mock_subprocess):
        """Test query with non-retryable error."""
        mock_process = Mock()
        mock_process.returncode = 1
        mock_process.communicate = AsyncMock(return_value=(b"", b"Invalid API key"))
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        messages = []
        async for msg in transport._execute_query(command, env, options, "Test"):
            messages.append(msg)

        assert len(messages) == 1
        assert isinstance(messages[0], ResultMessage)
        assert messages[0].error is True
        assert "Invalid API key" in messages[0].message

    @pytest.mark.asyncio
    async def test_execute_query_error_retryable(self, transport, mock_subprocess):
        """Test query with retryable error."""
        mock_process = Mock()
        mock_process.returncode = 1
        mock_process.communicate = AsyncMock(return_value=(b"", b"Connection timeout"))
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        with pytest.raises(TransportError, match="retryable"):
            async for _ in transport._execute_query(command, env, options, "Test"):
                pass

    @pytest.mark.asyncio
    async def test_execute_query_empty_response(self, transport, mock_subprocess):
        """Test query with empty response."""
        mock_process = Mock()
        mock_process.returncode = 0
        mock_process.communicate = AsyncMock(return_value=(b"", b""))
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        messages = []
        async for msg in transport._execute_query(command, env, options, "Test"):
            messages.append(msg)

        # Should yield ResultMessage only
        assert len(messages) == 1
        assert isinstance(messages[0], ResultMessage)
        assert messages[0].error is False

    @pytest.mark.asyncio
    async def test_send_query_no_retry(self, transport):
        """Test send_query with retry disabled."""
        with patch.object(transport, "_execute_query") as mock_execute:

            async def mock_execute_impl(*args):
                yield GeminiMessage(content="Response")
                yield ResultMessage(error=False)

            mock_execute.side_effect = mock_execute_impl

            options = GeminiOptions(no_retry=True)
            messages = []
            async for msg in transport.send_query("Test", options):
                messages.append(msg)

            assert len(messages) == 2
            assert isinstance(messages[0], GeminiMessage)
            assert isinstance(messages[1], ResultMessage)

    @pytest.mark.asyncio
    async def test_send_query_with_retry_success(self, transport):
        """Test send_query with retry that succeeds on second attempt."""
        call_count = 0

        async def mock_execute(*args):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                msg = "Connection error"
                raise TransportError(msg)
            yield GeminiMessage(content="Success")
            yield ResultMessage(error=False)

        with patch.object(transport, "_execute_query", side_effect=mock_execute):
            options = GeminiOptions(retry_count=2, retry_delay=0.1)
            messages = []
            async for msg in transport.send_query("Test", options):
                messages.append(msg)

            assert call_count == 2
            assert len(messages) == 2
            assert len(messages[0].content) == 1
            assert messages[0].content[0].text == "Success"

    @pytest.mark.asyncio
    async def test_send_query_all_retries_fail(self, transport):
        """Test send_query when all retries fail."""

        async def mock_execute(*args):
            msg = "Network error"
            raise ConnectionError(msg)

        with patch.object(transport, "_execute_query", side_effect=mock_execute):
            options = GeminiOptions(retry_count=2, retry_delay=0.1)
            messages = []
            async for msg in transport.send_query("Test", options):
                messages.append(msg)

            assert len(messages) == 1
            assert isinstance(messages[0], ResultMessage)
            assert messages[0].error is True
            assert "failed after 2 retries" in messages[0].message

    @pytest.mark.asyncio
    async def test_send_query_empty_response_error(self, transport):
        """Test send_query when response is empty."""

        async def mock_execute(*args):
            # Yield nothing - empty response
            return
            yield  # Make it a generator

        with patch.object(transport, "_execute_query", side_effect=mock_execute):
            options = GeminiOptions(retry_count=1, retry_delay=0.1)
            messages = []
            async for msg in transport.send_query("Test", options):
                messages.append(msg)

            # Should get error after retries
            assert len(messages) == 1
            assert isinstance(messages[0], ResultMessage)
            assert messages[0].error is True

    @pytest.mark.asyncio
    async def test_process_tracking(self, transport, mock_subprocess):
        """Test that process is properly tracked during execution."""
        mock_process = Mock()
        mock_process.returncode = 0
        mock_process.communicate = AsyncMock(return_value=(b'{"content": "Test"}', b""))
        mock_subprocess.return_value = mock_process

        command = ["gemini", "-p", "Test"]
        env = {"PATH": "/bin"}
        options = GeminiOptions()

        # Process should be None initially
        assert transport.process is None

        messages = []
        async for msg in transport._execute_query(command, env, options, "Test"):
            messages.append(msg)
            # Process should be set during execution
            if isinstance(msg, GeminiMessage):
                assert transport.process is mock_process

        # Process remains set after execution (cleanup happens in disconnect)
        assert transport.process is mock_process
