"""
Unit tests for foundry_mcp.core.providers implementation modules.

Tests cover each provider (Gemini, Codex, Cursor Agent, Claude, OpenCode) with:
- Provider instantiation and configuration
- Model validation and selection
- Request validation
- Command building and subprocess invocation (mocked)
- Output parsing and token usage extraction
- Error handling (timeout, unavailable, execution errors)
- Streaming support
"""

import json
import subprocess
from pathlib import Path
from typing import Dict, Optional, Sequence
from unittest.mock import patch

import pytest

from foundry_mcp.core.providers.base import (
    ProviderExecutionError,
    ProviderHooks,
    ProviderRequest,
    ProviderStatus,
    ProviderTimeoutError,
    ProviderUnavailableError,
)

# =============================================================================
# Test Fixtures and Helpers
# =============================================================================


def make_mock_runner(
    stdout: str = "",
    stderr: str = "",
    returncode: int = 0,
    raises: Optional[Exception] = None,
):
    """Create a mock runner that returns specified subprocess result."""

    def runner(
        command: Sequence[str],
        *,
        timeout: Optional[int] = None,
        env: Optional[Dict[str, str]] = None,
        input_data: Optional[str] = None,
    ):
        if raises:
            raise raises
        return subprocess.CompletedProcess(
            args=list(command),
            returncode=returncode,
            stdout=stdout,
            stderr=stderr,
        )

    return runner


@pytest.fixture
def hooks():
    """Provide default empty hooks."""
    return ProviderHooks()


@pytest.fixture
def stream_chunks():
    """Track streaming chunks for tests."""
    chunks = []

    def on_chunk(chunk, metadata):
        chunks.append(chunk)

    return chunks, on_chunk


# =============================================================================
# GeminiProvider Tests
# =============================================================================


class TestGeminiProvider:
    """Tests for GeminiProvider implementation."""

    def test_instantiation_default_model(self, hooks):
        """GeminiProvider should use default model when none specified."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks)
        assert provider._model == "pro"

    def test_instantiation_custom_model(self, hooks):
        """GeminiProvider should accept valid custom model."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, model="pro")
        assert provider._model == "pro"

    def test_custom_binary(self, hooks):
        """GeminiProvider should accept custom binary path."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, binary="/custom/gemini")
        assert provider._binary == "/custom/gemini"

    def test_validate_request_warns_on_unsupported_params(self, hooks, caplog):
        """GeminiProvider should warn on unsupported request parameters."""
        import logging

        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks)
        request = ProviderRequest(prompt="test", temperature=0.5)

        with caplog.at_level(logging.WARNING):
            provider._validate_request(request)

        assert "ignoring unsupported parameters" in caplog.text.lower()

    def test_build_command_includes_allowed_tools(self, hooks):
        """GeminiProvider command should include allowed tools (prompt via stdin)."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks)
        command = provider._build_command("pro")

        assert "gemini" in command[0]
        assert "--output-format" in command
        assert "json" in command
        assert "--allowed-tools" in command
        # Note: prompt is now passed via stdin, not CLI arg

    def test_successful_execution(self, hooks):
        """GeminiProvider should return valid result on success."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        mock_output = json.dumps(
            {
                "response": "Hello from Gemini!",
                "model": "gemini-2.5-flash",
                "stats": {"models": {"gemini-2.5-flash": {"tokens": {"prompt": 10, "candidates": 5, "total": 15}}}},
            }
        )
        runner = make_mock_runner(stdout=mock_output)
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        result = provider.generate(ProviderRequest(prompt="Hello"))

        assert result.content == "Hello from Gemini!"
        assert result.status == ProviderStatus.SUCCESS
        assert result.provider_id == "gemini"
        assert result.tokens.input_tokens == 10
        assert result.tokens.output_tokens == 5

    def test_nonzero_exit_raises_execution_error(self, hooks):
        """GeminiProvider should raise on non-zero exit code."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        runner = make_mock_runner(returncode=1, stderr="Command failed")
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="exited with code 1"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_empty_output_raises_execution_error(self, hooks):
        """GeminiProvider should raise on empty output."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        runner = make_mock_runner(stdout="")
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="empty output"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_invalid_json_raises_execution_error(self, hooks):
        """GeminiProvider should raise on invalid JSON output."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        runner = make_mock_runner(stdout="not valid json")
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="invalid JSON"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_timeout_raises_timeout_error(self, hooks):
        """GeminiProvider should raise timeout error when subprocess times out."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        runner = make_mock_runner(raises=subprocess.TimeoutExpired("gemini", 30))
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderTimeoutError):
            provider.generate(ProviderRequest(prompt="test"))

    def test_file_not_found_raises_unavailable_error(self, hooks):
        """GeminiProvider should raise unavailable error when binary not found."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        runner = make_mock_runner(raises=FileNotFoundError("gemini not found"))
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderUnavailableError, match="not available"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_streaming_emits_chunk(self, hooks, stream_chunks):
        """GeminiProvider should emit stream chunk when streaming enabled."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        chunks, on_chunk = stream_chunks
        hooks = ProviderHooks(on_stream_chunk=on_chunk)

        mock_output = json.dumps({"response": "Streamed content"})
        runner = make_mock_runner(stdout=mock_output)
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        result = provider.generate(ProviderRequest(prompt="test", stream=True))

        assert result.content == "Streamed content"
        assert len(chunks) == 1
        assert chunks[0].content == "Streamed content"

    def test_model_override_in_request(self, hooks):
        """GeminiProvider should use model from request metadata."""
        from foundry_mcp.core.providers.gemini import GEMINI_METADATA, GeminiProvider

        mock_output = json.dumps({"response": "OK", "model": "pro"})
        runner = make_mock_runner(stdout=mock_output)
        provider = GeminiProvider(metadata=GEMINI_METADATA, hooks=hooks, runner=runner)

        request = ProviderRequest(prompt="test", metadata={"model": "pro"})
        result = provider.generate(request)

        assert "pro" in result.model_used


# =============================================================================
# CodexProvider Tests
# =============================================================================


class TestCodexProvider:
    """Tests for CodexProvider implementation."""

    def test_instantiation_default_model(self, hooks):
        """CodexProvider should use default model when none specified."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks)
        assert provider._model == "gpt-5.2"

    def test_build_command_includes_sandbox(self, hooks):
        """CodexProvider command should include sandbox flags and stdin marker."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks)
        command = provider._build_command("gpt-5.1-codex", [])

        assert "codex" in command[0]
        assert "exec" in command
        assert "--sandbox" in command
        assert "read-only" in command
        assert "--json" in command
        assert "-" in command  # stdin marker for prompt

    def test_successful_execution_jsonl(self, hooks):
        """CodexProvider should parse JSONL output correctly."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        # Simulate Codex JSONL output
        events = [
            {"type": "thread.started", "thread_id": "t123"},
            {"type": "item.delta", "content": {"text": "Hello"}},
            {"type": "item.completed", "content": {"text": "Hello from Codex!"}},
            {
                "type": "turn.completed",
                "usage": {"input_tokens": 10, "output_tokens": 20, "total_tokens": 30},
            },
        ]
        mock_output = "\n".join(json.dumps(e) for e in events)

        runner = make_mock_runner(stdout=mock_output)
        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks, runner=runner)

        result = provider.generate(ProviderRequest(prompt="Hello"))

        assert result.content == "Hello from Codex!"
        assert result.status == ProviderStatus.SUCCESS
        assert result.tokens.input_tokens == 10
        assert result.tokens.output_tokens == 20

    def test_nonzero_exit_raises_execution_error(self, hooks):
        """CodexProvider should raise on non-zero exit code."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        runner = make_mock_runner(returncode=1, stderr="Sandbox error")
        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="exited with code 1"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_empty_output_raises_execution_error(self, hooks):
        """CodexProvider should raise on empty output."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        runner = make_mock_runner(stdout="")
        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="empty output"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_invalid_jsonl_raises_execution_error(self, hooks):
        """CodexProvider should raise on invalid JSONL."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        runner = make_mock_runner(stdout="not valid json\nmore bad data")
        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="invalid JSON"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_attachment_handling(self, hooks):
        """CodexProvider should add attachments as --image flags."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks)
        command = provider._build_command("gpt-5.1-codex", ["/path/to/image.png"])

        assert "--image" in command
        assert "/path/to/image.png" in command
        assert "-" in command  # stdin marker (prompt passed separately)

    def test_streaming_emits_deltas(self, hooks, stream_chunks):
        """CodexProvider should emit stream chunks for item.delta events."""
        from foundry_mcp.core.providers.codex import CODEX_METADATA, CodexProvider

        chunks, on_chunk = stream_chunks
        hooks = ProviderHooks(on_stream_chunk=on_chunk)

        events = [
            {"type": "item.delta", "content": {"text": "chunk1"}},
            {"type": "item.delta", "content": {"text": "chunk2"}},
            {"type": "item.completed", "content": {"text": "chunk1chunk2"}},
        ]
        mock_output = "\n".join(json.dumps(e) for e in events)

        runner = make_mock_runner(stdout=mock_output)
        provider = CodexProvider(metadata=CODEX_METADATA, hooks=hooks, runner=runner)

        provider.generate(ProviderRequest(prompt="test", stream=True))

        assert len(chunks) == 2
        assert chunks[0].content == "chunk1"
        assert chunks[1].content == "chunk2"


# =============================================================================
# CursorAgentProvider Tests
# =============================================================================


class TestCursorAgentProvider:
    """Tests for CursorAgentProvider implementation."""

    def test_instantiation_default_model(self, hooks):
        """CursorAgentProvider should use default model when none specified."""
        from foundry_mcp.core.providers.cursor_agent import (
            CURSOR_METADATA,
            CursorAgentProvider,
        )

        provider = CursorAgentProvider(metadata=CURSOR_METADATA, hooks=hooks)
        assert provider._model == "composer-1"

    def test_build_command_includes_print_and_json(self, hooks):
        """CursorAgentProvider command should include --print and output format (prompt via stdin)."""
        from foundry_mcp.core.providers.cursor_agent import (
            CURSOR_METADATA,
            CursorAgentProvider,
        )

        provider = CursorAgentProvider(metadata=CURSOR_METADATA, hooks=hooks)
        request = ProviderRequest(prompt="test")
        command, prompt = provider._build_command(request, "composer-1")

        assert "--print" in command
        assert "--output-format" in command
        assert "json" in command
        assert "test" in prompt  # Prompt returned separately for stdin

    def test_successful_execution_json_mode(self, hooks, tmp_path):
        """CursorAgentProvider should parse JSON output correctly."""
        from foundry_mcp.core.providers.cursor_agent import (
            CURSOR_METADATA,
            CursorAgentProvider,
        )

        mock_output = json.dumps(
            {
                "result": "Hello from Cursor!",
                "model": "composer-1",
                "usage": {"input_tokens": 15, "output_tokens": 8, "total_tokens": 23},
            }
        )
        runner = make_mock_runner(stdout=mock_output)

        # Create mock cursor config dir
        with patch.object(Path, "home", return_value=tmp_path):
            (tmp_path / ".cursor").mkdir(exist_ok=True)
            provider = CursorAgentProvider(metadata=CURSOR_METADATA, hooks=hooks, runner=runner)

            result = provider.generate(ProviderRequest(prompt="Hello"))

            assert result.content == "Hello from Cursor!"
            assert result.status == ProviderStatus.SUCCESS
            assert result.tokens.input_tokens == 15

    def test_attachments_not_supported(self, hooks, tmp_path):
        """CursorAgentProvider should reject attachments."""
        from foundry_mcp.core.providers.cursor_agent import (
            CURSOR_METADATA,
            CursorAgentProvider,
        )

        with patch.object(Path, "home", return_value=tmp_path):
            (tmp_path / ".cursor").mkdir(exist_ok=True)
            provider = CursorAgentProvider(metadata=CURSOR_METADATA, hooks=hooks)

            request = ProviderRequest(prompt="test", attachments=["image.png"])

            with pytest.raises(ProviderExecutionError, match="does not support"):
                provider.generate(request)

    def test_fallback_to_text_mode(self, hooks, tmp_path):
        """CursorAgentProvider should fall back when --output-format not supported."""
        from foundry_mcp.core.providers.cursor_agent import (
            CURSOR_METADATA,
            CursorAgentProvider,
        )

        # First call fails with unknown option, retry succeeds
        call_count = [0]

        def runner(command, *, timeout=None, env=None, input_data=None):
            call_count[0] += 1
            if call_count[0] == 1:
                return subprocess.CompletedProcess(
                    args=list(command),
                    returncode=1,
                    stdout="",
                    stderr="unknown option: --output-format",
                )
            return subprocess.CompletedProcess(
                args=list(command),
                returncode=0,
                stdout="Plain text response",
                stderr="",
            )

        with patch.object(Path, "home", return_value=tmp_path):
            (tmp_path / ".cursor").mkdir(exist_ok=True)
            provider = CursorAgentProvider(metadata=CURSOR_METADATA, hooks=hooks, runner=runner)

            result = provider.generate(ProviderRequest(prompt="test"))

            assert result.content == "Plain text response"
            assert call_count[0] == 2  # Retry happened


# =============================================================================
# ClaudeProvider Tests
# =============================================================================


class TestClaudeProvider:
    """Tests for ClaudeProvider implementation."""

    def test_instantiation_default_model(self, hooks):
        """ClaudeProvider should use default model when none specified."""
        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks)
        assert provider._model == "opus"

    def test_build_command_includes_allowed_and_disallowed(self, hooks):
        """ClaudeProvider command should include tool restrictions (prompt via stdin)."""
        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks)
        command = provider._build_command("sonnet")

        assert "--print" in command
        assert "--output-format" in command
        assert "json" in command
        assert "--allowed-tools" in command
        assert "--disallowed-tools" in command
        assert "--system-prompt" in command
        # Note: prompt is now passed via stdin, not CLI arg

    def test_successful_execution(self, hooks):
        """ClaudeProvider should return valid result on success."""
        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        mock_output = json.dumps(
            {
                "result": "Hello from Claude!",
                "modelUsage": {"claude-sonnet-4-5-20250929": {}},
                "usage": {"input_tokens": 12, "output_tokens": 6},
            }
        )
        runner = make_mock_runner(stdout=mock_output)
        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks, runner=runner)

        result = provider.generate(ProviderRequest(prompt="Hello"))

        assert result.content == "Hello from Claude!"
        assert result.status == ProviderStatus.SUCCESS
        assert result.tokens.input_tokens == 12
        assert result.tokens.output_tokens == 6

    def test_validate_request_warns_on_unsupported_params(self, hooks, caplog):
        """ClaudeProvider should warn on unsupported request parameters."""
        import logging

        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks)
        request = ProviderRequest(prompt="test", temperature=0.5)

        with caplog.at_level(logging.WARNING):
            provider._validate_request(request)

        assert "ignoring unsupported parameters" in caplog.text.lower()

    def test_nonzero_exit_raises_execution_error(self, hooks):
        """ClaudeProvider should raise on non-zero exit code."""
        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        runner = make_mock_runner(returncode=1, stderr="Auth failed")
        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks, runner=runner)

        with pytest.raises(ProviderExecutionError, match="exited with code 1"):
            provider.generate(ProviderRequest(prompt="test"))

    def test_streaming_emits_chunk(self, hooks, stream_chunks):
        """ClaudeProvider should emit stream chunk when streaming enabled."""
        from foundry_mcp.core.providers.claude import CLAUDE_METADATA, ClaudeProvider

        chunks, on_chunk = stream_chunks
        hooks = ProviderHooks(on_stream_chunk=on_chunk)

        mock_output = json.dumps({"result": "Streamed"})
        runner = make_mock_runner(stdout=mock_output)
        provider = ClaudeProvider(metadata=CLAUDE_METADATA, hooks=hooks, runner=runner)

        provider.generate(ProviderRequest(prompt="test", stream=True))

        assert len(chunks) == 1
        assert chunks[0].content == "Streamed"


# =============================================================================
# OpenCodeProvider Tests
# =============================================================================


class TestOpenCodeProvider:
    """Tests for OpenCodeProvider implementation."""

    def test_instantiation_default_model(self, hooks):
        """OpenCodeProvider should use default model when none specified."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        provider = OpenCodeProvider(metadata=OPENCODE_METADATA, hooks=hooks)
        assert provider._model == "openai/gpt-5.1-codex-mini"

    def test_validate_request_rejects_attachments(self, hooks):
        """OpenCodeProvider should reject attachments."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        provider = OpenCodeProvider(metadata=OPENCODE_METADATA, hooks=hooks)
        request = ProviderRequest(prompt="test", attachments=["image.png"])

        with pytest.raises(ProviderExecutionError, match="does not support"):
            provider._validate_request(request)

    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_opencode_server_healthy")
    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_port_open")
    def test_successful_execution(self, mock_port, mock_healthy, hooks, tmp_path):
        """OpenCodeProvider should parse line-delimited JSON output."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        mock_port.return_value = True  # Server already running
        mock_healthy.return_value = True  # Server is healthy

        # Note: done response text takes precedence when content_parts is empty
        output_lines = [
            {
                "type": "done",
                "response": {
                    "text": "Hello from OpenCode!",
                    "model": "gpt-4",
                    "usage": {
                        "prompt_tokens": 8,
                        "completion_tokens": 4,
                        "total_tokens": 12,
                    },
                },
            },
        ]
        mock_output = "\n".join(json.dumps(line) for line in output_lines)

        runner = make_mock_runner(stdout=mock_output)
        provider = OpenCodeProvider(
            metadata=OPENCODE_METADATA,
            hooks=hooks,
            runner=runner,
            wrapper_path=tmp_path / "wrapper.js",
        )

        result = provider.generate(ProviderRequest(prompt="Hello"))

        assert result.content == "Hello from OpenCode!"
        assert result.status == ProviderStatus.SUCCESS
        assert result.tokens.input_tokens == 8
        assert result.tokens.output_tokens == 4

    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_opencode_server_healthy")
    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_port_open")
    def test_wrapper_error_raises_execution_error(self, mock_port, mock_healthy, hooks, tmp_path):
        """OpenCodeProvider should raise when wrapper returns error event."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        mock_port.return_value = True
        mock_healthy.return_value = True

        output_lines = [{"type": "error", "message": "API key invalid"}]
        mock_output = "\n".join(json.dumps(line) for line in output_lines)

        runner = make_mock_runner(stdout=mock_output)
        provider = OpenCodeProvider(
            metadata=OPENCODE_METADATA,
            hooks=hooks,
            runner=runner,
            wrapper_path=tmp_path / "wrapper.js",
        )

        with pytest.raises(ProviderExecutionError, match="API key invalid"):
            provider.generate(ProviderRequest(prompt="test"))

    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_opencode_server_healthy")
    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_port_open")
    def test_nonzero_exit_raises_execution_error(self, mock_port, mock_healthy, hooks, tmp_path):
        """OpenCodeProvider should raise on non-zero exit code."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        mock_port.return_value = True
        mock_healthy.return_value = True

        runner = make_mock_runner(returncode=1, stderr="Node error")
        provider = OpenCodeProvider(
            metadata=OPENCODE_METADATA,
            hooks=hooks,
            runner=runner,
            wrapper_path=tmp_path / "wrapper.js",
        )

        with pytest.raises(ProviderExecutionError, match="exited with code 1"):
            provider.generate(ProviderRequest(prompt="test"))

    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_opencode_server_healthy")
    @patch("foundry_mcp.core.providers.opencode.OpenCodeProvider._is_port_open")
    def test_streaming_emits_chunks(self, mock_port, mock_healthy, hooks, stream_chunks, tmp_path):
        """OpenCodeProvider should emit stream chunks."""
        from foundry_mcp.core.providers.opencode import (
            OPENCODE_METADATA,
            OpenCodeProvider,
        )

        mock_port.return_value = True
        mock_healthy.return_value = True
        chunks, on_chunk = stream_chunks
        hooks = ProviderHooks(on_stream_chunk=on_chunk)

        output_lines = [
            {"type": "chunk", "content": "part1"},
            {"type": "chunk", "content": "part2"},
            {"type": "done", "response": {"text": "part1part2"}},
        ]
        mock_output = "\n".join(json.dumps(line) for line in output_lines)

        runner = make_mock_runner(stdout=mock_output)
        provider = OpenCodeProvider(
            metadata=OPENCODE_METADATA,
            hooks=hooks,
            runner=runner,
            wrapper_path=tmp_path / "wrapper.js",
        )

        provider.generate(ProviderRequest(prompt="test", stream=True))

        assert len(chunks) >= 2


# =============================================================================
# is_*_available Function Tests
# =============================================================================


class TestAvailabilityFunctions:
    """Tests for provider availability check functions."""

    @patch("foundry_mcp.core.providers.gemini.detect_provider_availability")
    def test_is_gemini_available(self, mock_detect):
        """is_gemini_available should delegate to detector."""
        from foundry_mcp.core.providers.gemini import is_gemini_available

        mock_detect.return_value = True
        assert is_gemini_available() is True
        mock_detect.assert_called_with("gemini")

    @patch("foundry_mcp.core.providers.codex.detect_provider_availability")
    def test_is_codex_available(self, mock_detect):
        """is_codex_available should delegate to detector."""
        from foundry_mcp.core.providers.codex import is_codex_available

        mock_detect.return_value = False
        assert is_codex_available() is False
        mock_detect.assert_called_with("codex")

    @patch("foundry_mcp.core.providers.cursor_agent.detect_provider_availability")
    def test_is_cursor_agent_available(self, mock_detect):
        """is_cursor_agent_available should delegate to detector."""
        from foundry_mcp.core.providers.cursor_agent import is_cursor_agent_available

        mock_detect.return_value = True
        assert is_cursor_agent_available() is True
        mock_detect.assert_called_with("cursor-agent")

    @patch("foundry_mcp.core.providers.claude.detect_provider_availability")
    def test_is_claude_available(self, mock_detect):
        """is_claude_available should delegate to detector."""
        from foundry_mcp.core.providers.claude import is_claude_available

        mock_detect.return_value = True
        assert is_claude_available() is True
        mock_detect.assert_called_with("claude")

    @patch("foundry_mcp.core.providers.opencode.detect_provider_availability")
    def test_is_opencode_available(self, mock_detect):
        """is_opencode_available should delegate to detector."""
        from foundry_mcp.core.providers.opencode import is_opencode_available

        mock_detect.return_value = False
        assert is_opencode_available() is False
        mock_detect.assert_called_with("opencode")


# =============================================================================
# create_provider Factory Tests
# =============================================================================


class TestCreateProviderFactories:
    """Tests for create_provider factory functions."""

    def test_gemini_create_provider(self, hooks):
        """Gemini create_provider should return configured provider."""
        from foundry_mcp.core.providers.gemini import create_provider

        provider = create_provider(hooks=hooks, model="pro")
        assert provider._model == "pro"

    def test_codex_create_provider(self, hooks):
        """Codex create_provider should return configured provider."""
        from foundry_mcp.core.providers.codex import create_provider

        provider = create_provider(hooks=hooks)
        assert provider._model == "gpt-5.2"

    def test_cursor_agent_create_provider(self, hooks):
        """Cursor Agent create_provider should return configured provider."""
        from foundry_mcp.core.providers.cursor_agent import create_provider

        provider = create_provider(hooks=hooks)
        assert provider._model == "composer-1"

    def test_claude_create_provider(self, hooks):
        """Claude create_provider should return configured provider."""
        from foundry_mcp.core.providers.claude import create_provider

        provider = create_provider(hooks=hooks, model="haiku")
        assert provider._model == "haiku"

    def test_opencode_create_provider(self, hooks):
        """OpenCode create_provider should return configured provider."""
        from foundry_mcp.core.providers.opencode import create_provider

        provider = create_provider(hooks=hooks)
        assert provider._model == "openai/gpt-5.1-codex-mini"

    def test_factory_accepts_dependencies_and_overrides(self, hooks):
        """Factory should inject dependencies and apply overrides."""
        from foundry_mcp.core.providers.gemini import create_provider

        custom_runner = make_mock_runner(stdout='{"response": "test"}')
        custom_env = {"GEMINI_API_KEY": "test-key"}

        provider = create_provider(
            hooks=hooks,
            dependencies={"runner": custom_runner, "env": custom_env},
            overrides={"binary": "/custom/gemini", "model": "pro"},
        )

        assert provider._binary == "/custom/gemini"
        assert provider._model == "pro"
        assert provider._env == custom_env


# =============================================================================
# Provider Registration Tests
# =============================================================================


class TestProviderRegistration:
    """Tests for automatic provider registration."""

    def test_gemini_registered(self):
        """Gemini provider should be registered."""
        from foundry_mcp.core.providers.registry import get_registration

        reg = get_registration("gemini")
        assert reg is not None
        assert reg.metadata.provider_id == "gemini"
        assert "cli" in reg.tags

    def test_codex_registered(self):
        """Codex provider should be registered."""
        from foundry_mcp.core.providers.registry import get_registration

        reg = get_registration("codex")
        assert reg is not None
        assert reg.metadata.provider_id == "codex"
        assert "sandboxed" in reg.tags

    def test_cursor_agent_registered(self):
        """Cursor Agent provider should be registered."""
        from foundry_mcp.core.providers.registry import get_registration

        reg = get_registration("cursor-agent")
        assert reg is not None
        assert reg.metadata.provider_id == "cursor-agent"

    def test_claude_registered(self):
        """Claude provider should be registered."""
        from foundry_mcp.core.providers.registry import get_registration

        reg = get_registration("claude")
        assert reg is not None
        assert reg.metadata.provider_id == "claude"
        assert "thinking" in reg.tags

    def test_opencode_registered(self):
        """OpenCode provider should be registered."""
        from foundry_mcp.core.providers.registry import get_registration

        reg = get_registration("opencode")
        assert reg is not None
        assert reg.metadata.provider_id == "opencode"
        assert "sdk" in reg.tags
