"""
Integration Tests for CLI Adapters.

These tests verify that the CLI adapters:
1. Build commands correctly
2. Parse responses correctly
3. Handle errors gracefully
4. Work with mock subprocess calls

Note: These tests mock subprocess calls to avoid requiring actual CLI tools.
For real integration tests, set INTEGRATION_TEST=1 environment variable.
"""

import asyncio
import json
import os
import pytest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

from agent_orchestrator.adapters.claude_code_cli import (
    ClaudeCodeCLIAdapter,
    ClaudeCodeConfig,
    create_claude_code_adapter,
)
from agent_orchestrator.adapters.gemini_cli import (
    GeminiCLIAdapter,
    GeminiCLIConfig,
    create_gemini_adapter,
)
from agent_orchestrator.adapters.codex_cli import (
    CodexCLIAdapter,
    CodexCLIConfig,
    AutonomyMode,
    create_codex_adapter,
)
from agent_orchestrator.adapters.base import AgentStatus


# Skip real integration tests unless explicitly enabled
INTEGRATION_TEST = os.environ.get("INTEGRATION_TEST", "0") == "1"


class TestClaudeCodeCLIAdapter:
    """Tests for Claude Code CLI adapter."""

    @pytest.fixture
    def adapter(self, tmp_path):
        """Create a Claude Code adapter for testing."""
        return ClaudeCodeCLIAdapter(
            agent_id="test-claude",
            workspace_path=str(tmp_path),
        )

    @pytest.fixture
    def mock_claude_available(self):
        """Mock claude CLI as available."""
        with patch("shutil.which", return_value="/usr/bin/claude"):
            yield

    def test_init(self, adapter):
        """Test adapter initialization."""
        assert adapter.agent_id == "test-claude"
        assert adapter.config.executable == "claude"
        assert adapter.config.output_format == "json"
        assert adapter.config.timeout_seconds == 600

    def test_init_with_custom_config(self, tmp_path):
        """Test adapter with custom configuration."""
        config = ClaudeCodeConfig(
            executable="/custom/claude",
            timeout_seconds=300,
            max_turns=5,
        )
        adapter = ClaudeCodeCLIAdapter(
            agent_id="test-claude",
            workspace_path=str(tmp_path),
            config=config,
        )
        assert adapter.config.executable == "/custom/claude"
        assert adapter.config.timeout_seconds == 300
        assert adapter.config.max_turns == 5

    def test_build_prompt_with_context(self, adapter):
        """Test prompt building with full context."""
        context = {
            "project_state": {"version": "1.0.0"},
            "constraints": ["No breaking changes", "Follow PEP8"],
            "recent_decisions": ["Use SQLite for storage"],
            "active_objectives": [
                {"description": "Implement auth", "status": "in_progress"}
            ],
        }
        prompt = adapter._build_prompt("Write tests", context)

        assert "## Orchestration Context" in prompt
        assert "## Current Project State" in prompt
        assert '"version": "1.0.0"' in prompt
        assert "## Constraints" in prompt
        assert "No breaking changes" in prompt
        assert "## Recent Decisions" in prompt
        assert "Use SQLite for storage" in prompt
        assert "## Active Objectives" in prompt
        assert "Implement auth" in prompt
        assert "## Your Task" in prompt
        assert "Write tests" in prompt

    def test_build_command(self, adapter):
        """Test command building."""
        cmd = adapter._build_command("Test prompt")

        assert cmd[0] == "claude"
        assert "-p" in cmd
        assert "Test prompt" in cmd
        assert "--output-format" in cmd
        assert "json" in cmd

    def test_build_command_streaming(self, adapter):
        """Test command building for streaming mode."""
        cmd = adapter._build_command("Test prompt", stream=True)

        assert "--output-format" in cmd
        assert "stream-json" in cmd

    def test_parse_response_json(self, adapter):
        """Test parsing JSON response."""
        result = {
            "content": "Task completed successfully",
            "usage": {
                "input_tokens": 100,
                "output_tokens": 50,
            },
            "files_modified": ["test.py", "utils.py"],
        }

        response = adapter._parse_response(result, "Test task")

        assert response.success
        assert response.content == "Task completed successfully"
        assert response.tokens_input == 100
        assert response.tokens_output == 50
        assert "test.py" in response.artifacts.files_modified

    def test_parse_response_raw_output(self, adapter):
        """Test parsing raw (non-JSON) response."""
        result = {
            "content": "Raw text output",
            "raw_output": True,
        }

        response = adapter._parse_response(result, "Test task")

        assert response.success
        assert response.content == "Raw text output"
        assert response.metadata["raw_output"] is True

    @pytest.mark.asyncio
    async def test_execute_no_claude(self, adapter):
        """Test execute when Claude is not available."""
        adapter._claude_available = False

        response = await adapter.execute("Test task", {})

        assert not response.success
        assert "not found" in response.error.lower()

    @pytest.mark.asyncio
    async def test_execute_success(self, adapter, mock_claude_available):
        """Test successful execution with mocked subprocess."""
        adapter._claude_available = True

        mock_result = {
            "content": "Tests written successfully",
            "usage": {"input_tokens": 200, "output_tokens": 100},
            "files_modified": ["test_auth.py"],
        }

        with patch("asyncio.create_subprocess_exec") as mock_exec:
            mock_process = AsyncMock()
            mock_process.communicate = AsyncMock(
                return_value=(json.dumps(mock_result).encode(), b"")
            )
            mock_process.returncode = 0
            mock_exec.return_value = mock_process

            response = await adapter.execute("Write auth tests", {})

            assert response.success
            assert "Tests written" in response.content
            assert response.tokens_input == 200

    @pytest.mark.asyncio
    async def test_execute_timeout(self, adapter, mock_claude_available):
        """Test execution timeout handling."""
        adapter._claude_available = True
        adapter.config.timeout_seconds = 1

        with patch("asyncio.create_subprocess_exec") as mock_exec:
            mock_process = AsyncMock()

            async def slow_communicate():
                await asyncio.sleep(10)
                return (b"", b"")

            mock_process.communicate = slow_communicate
            mock_process.kill = MagicMock()
            mock_process.wait = AsyncMock()
            mock_exec.return_value = mock_process

            response = await adapter.execute("Slow task", {})

            assert not response.success
            assert "timed out" in response.error.lower()


class TestGeminiCLIAdapter:
    """Tests for Gemini CLI adapter."""

    @pytest.fixture
    def adapter(self, tmp_path):
        """Create a Gemini adapter for testing."""
        return GeminiCLIAdapter(
            agent_id="test-gemini",
            workspace_path=str(tmp_path),
        )

    def test_init(self, adapter):
        """Test adapter initialization."""
        assert adapter.agent_id == "test-gemini"
        assert adapter.config.executable == "gemini"
        assert adapter.config.search_grounding is True
        assert adapter.config.max_context_tokens == 100000

    def test_build_prompt_with_search_grounding(self, adapter):
        """Test prompt includes search grounding note."""
        prompt = adapter._build_prompt("Research best practices", {})

        assert "search grounding" in prompt.lower()

    def test_build_command_with_mcp_servers(self, tmp_path):
        """Test command building with MCP servers."""
        config = GeminiCLIConfig(
            mcp_servers=["filesystem", "git"],
        )
        adapter = GeminiCLIAdapter(
            agent_id="test-gemini",
            workspace_path=str(tmp_path),
            config=config,
        )

        cmd = adapter._build_command("Test prompt")

        assert "--mcp-server" in cmd
        assert "filesystem" in cmd
        assert "git" in cmd

    def test_parse_response_with_grounding(self, adapter):
        """Test parsing response with grounding metadata."""
        result = {
            "content": "Based on current best practices...",
            "usage": {
                "prompt_token_count": 500,
                "candidates_token_count": 200,
            },
            "grounding_metadata": {"sources": ["doc1", "doc2"]},
        }

        response = adapter._parse_response(result, "Research task")

        assert response.success
        assert response.tokens_input == 500
        assert response.tokens_output == 200
        assert response.metadata["search_grounding_used"] is True

    def test_factory_function(self, tmp_path):
        """Test factory function creates adapter correctly."""
        adapter = create_gemini_adapter(
            agent_id="factory-gemini",
            workspace_path=str(tmp_path),
            search_grounding=False,
            timeout_seconds=300,
        )

        assert adapter.agent_id == "factory-gemini"
        assert adapter.config.search_grounding is False
        assert adapter.config.timeout_seconds == 300


class TestCodexCLIAdapter:
    """Tests for Codex CLI adapter."""

    @pytest.fixture
    def adapter(self, tmp_path):
        """Create a Codex adapter for testing."""
        return CodexCLIAdapter(
            agent_id="test-codex",
            workspace_path=str(tmp_path),
            autonomy_mode="auto-edit",
        )

    def test_init_with_autonomy_mode(self, adapter):
        """Test adapter initialization with autonomy mode."""
        assert adapter.agent_id == "test-codex"
        assert adapter.config.autonomy_mode == AutonomyMode.AUTO_EDIT
        assert adapter.config.skip_git_check is True

    def test_autonomy_modes(self):
        """Test all autonomy mode values."""
        assert AutonomyMode.SUGGEST.value == "suggest"
        assert AutonomyMode.AUTO_EDIT.value == "auto-edit"
        assert AutonomyMode.FULL_AUTO.value == "full-auto"

    def test_approval_flags(self, tmp_path):
        """Test approval flags generation for each mode."""
        modes = [
            (AutonomyMode.SUGGEST, ["--ask-for-approval", "untrusted"], ["--sandbox", "read-only"]),
            (AutonomyMode.AUTO_EDIT, ["--ask-for-approval", "untrusted"], ["--sandbox", "workspace-write"]),
            (AutonomyMode.FULL_AUTO, [], ["--full-auto"]),
        ]

        for mode, expected_global, expected_exec in modes:
            config = CodexCLIConfig(autonomy_mode=mode)
            adapter = CodexCLIAdapter(
                agent_id="test",
                workspace_path=str(tmp_path),
                config=config,
            )
            assert adapter.config.approval_global_flags == expected_global
            assert adapter.config.approval_exec_flags == expected_exec

    def test_build_command_with_approval(self, adapter):
        """Test command includes approval flags and exec subcommand."""
        cmd = adapter._build_command("Test prompt")

        assert cmd[0] == "codex"
        assert "--ask-for-approval" in cmd
        assert "untrusted" in cmd
        assert "exec" in cmd
        assert "--model" in cmd
        assert "gpt-5-codex" in cmd
        assert "--sandbox" in cmd
        assert "workspace-write" in cmd
        assert "--skip-git-repo-check" in cmd

    def test_set_autonomy_mode(self, adapter):
        """Test changing autonomy mode."""
        adapter.set_autonomy_mode("full-auto")

        assert adapter.config.autonomy_mode == AutonomyMode.FULL_AUTO

    def test_factory_function(self, tmp_path):
        """Test factory function creates adapter correctly."""
        adapter = create_codex_adapter(
            agent_id="factory-codex",
            workspace_path=str(tmp_path),
            autonomy_mode="suggest",
            timeout_seconds=300,
        )

        assert adapter.agent_id == "factory-codex"
        assert adapter.config.autonomy_mode == AutonomyMode.SUGGEST
        assert adapter.config.timeout_seconds == 300

    def test_parse_response_with_cost(self, adapter):
        """Test cost calculation in response parsing."""
        result = {
            "content": "Code generated",
            "usage": {
                "prompt_tokens": 1000,
                "completion_tokens": 500,
            },
        }

        response = adapter._parse_response(result, "Generate code")

        assert response.success
        assert response.tokens_input == 1000
        assert response.tokens_output == 500
        # Check that cost is calculated (value depends on model pricing logic)
        assert response.cost > 0


class TestCLIAdapterIntegration:
    """Integration tests that run actual CLI tools (requires INTEGRATION_TEST=1)."""

    @pytest.mark.skipif(not INTEGRATION_TEST, reason="Integration tests disabled")
    @pytest.mark.asyncio
    async def test_claude_code_real_execution(self, tmp_path):
        """Test real Claude Code execution."""
        adapter = ClaudeCodeCLIAdapter(
            agent_id="integration-claude",
            workspace_path=str(tmp_path),
        )

        if not adapter._claude_available:
            pytest.skip("Claude CLI not available")

        response = await adapter.execute("What is 2+2?", {})

        assert response.success
        assert "4" in response.content

    @pytest.mark.skipif(not INTEGRATION_TEST, reason="Integration tests disabled")
    @pytest.mark.asyncio
    async def test_gemini_real_execution(self, tmp_path):
        """Test real Gemini CLI execution."""
        adapter = GeminiCLIAdapter(
            agent_id="integration-gemini",
            workspace_path=str(tmp_path),
        )

        if not adapter._gemini_available:
            pytest.skip("Gemini CLI not available")

        response = await adapter.execute("What is 2+2?", {})

        assert response.success
        assert "4" in response.content

    @pytest.mark.skipif(not INTEGRATION_TEST, reason="Integration tests disabled")
    @pytest.mark.asyncio
    async def test_codex_real_execution(self, tmp_path):
        """Test real Codex CLI execution."""
        adapter = CodexCLIAdapter(
            agent_id="integration-codex",
            workspace_path=str(tmp_path),
            autonomy_mode="suggest",
        )

        if not adapter._codex_available:
            pytest.skip("Codex CLI not available")

        response = await adapter.execute("What is 2+2?", {})

        assert response.success


class TestAdapterHealthAndStatus:
    """Tests for adapter health checking and status management."""

    @pytest.fixture
    def adapters(self, tmp_path):
        """Create all adapter types for testing."""
        return {
            "claude": ClaudeCodeCLIAdapter("test-claude", str(tmp_path)),
            "gemini": GeminiCLIAdapter("test-gemini", str(tmp_path)),
            "codex": CodexCLIAdapter("test-codex", str(tmp_path)),
        }

    def test_initial_status(self, adapters):
        """Test adapters start in IDLE status."""
        for name, adapter in adapters.items():
            assert adapter._status == AgentStatus.IDLE

    def test_get_name(self, adapters):
        """Test adapter names."""
        assert "Claude" in adapters["claude"].get_name()
        assert "Gemini" in adapters["gemini"].get_name()
        assert "Codex" in adapters["codex"].get_name()

    @pytest.mark.asyncio
    async def test_cancel(self, adapters):
        """Test cancel method."""
        for name, adapter in adapters.items():
            result = await adapter.cancel()
            assert result is True
            assert adapter._status == AgentStatus.IDLE

    def test_write_status_packet_idle(self, adapters):
        """Test status packet when idle."""
        for name, adapter in adapters.items():
            packet = adapter.write_status_packet()

            assert packet.agent_id == adapter.agent_id
            assert packet.status == "idle"


class TestSecretRedactionInAdapters:
    """Test that adapters properly redact secrets."""

    @pytest.fixture
    def adapter(self, tmp_path):
        return ClaudeCodeCLIAdapter("test-claude", str(tmp_path))

    def test_response_content_redaction(self, adapter):
        """Test that secrets in response content are redacted."""
        result = {
            "content": "API key is sk-abc123xyz and password is secret123",
            "usage": {"input_tokens": 10, "output_tokens": 20},
        }

        response = adapter._parse_response(result, "Test task")

        # Check that API key pattern is redacted
        assert "sk-abc123xyz" not in response.content
        assert "[REDACTED" in response.content  # Matches [REDACTED_OPENAI_KEY], etc.

    def test_error_message_redaction(self, adapter):
        """Test that secrets in error messages are redacted."""
        adapter._claude_available = True

        with patch("asyncio.create_subprocess_exec") as mock_exec:
            mock_process = AsyncMock()
            mock_process.communicate = AsyncMock(
                return_value=(b"", b"Error: Invalid API key sk-secret123")
            )
            mock_process.returncode = 1
            mock_exec.return_value = mock_process

            # Run the test
            async def run_test():
                response = await adapter.execute("Test", {})
                return response

            response = asyncio.get_event_loop().run_until_complete(run_test())

            assert not response.success
            assert "sk-secret123" not in response.error
