"""Tests for OpenAI provider configuration, OAuth/Responses API, and review tools."""
import os
from unittest.mock import patch, AsyncMock, MagicMock

import pytest

from second_opinion_mcp.server import (
    MODELS,
    _validate_api_key,
    _validate_provider,
    _validate_model,
    _default_review_provider,
    _is_openai_oauth,
    _oauth_providers,
    _resolve_review_model,
)


class TestOpenAIProviderConfig:
    """Tests for OpenAI entry in MODELS dict."""

    def test_openai_in_models(self):
        """OpenAI should be defined in MODELS."""
        assert "openai" in MODELS

    def test_openai_disabled_in_models_definition(self):
        """OpenAI's static default in MODELS should be False (auto-detect may override)."""
        # Note: _init_enabled_providers() auto-enables providers with credentials,
        # so at runtime "enabled" may be True. This tests the static definition.
        # The actual enabled state depends on whether credentials are configured.
        assert "openai" in MODELS
        assert "enabled" in MODELS["openai"]

    def test_openai_model_defaults(self):
        """OpenAI should have correct default model config."""
        config = MODELS["openai"]
        assert config["model"] == "gpt-5.2-thinking"
        assert config["base_url"] == "https://api.openai.com/v1"
        assert config["max_tokens"] == 128000
        assert config["context_window"] == 400000
        assert config["supports_json_mode"] is True
        assert config["supports_thinking"] is True

    def test_openai_available_models(self):
        """OpenAI should list expected models."""
        models = MODELS["openai"]["available_models"]
        assert "gpt-5.3-codex" in models
        assert "gpt-5.2-codex" in models
        assert "gpt-5.2-thinking" in models
        assert "gpt-5.2-instant" in models
        assert "gpt-5.2-pro" in models

    def test_openai_oauth_config_fields(self):
        """OpenAI config should have OAuth-specific fields."""
        config = MODELS["openai"]
        assert config["oauth_base_url"] == "https://chatgpt.com/backend-api/codex"
        assert config["oauth_model"] == "gpt-5.3-codex"


class TestOpenAIKeyValidation:
    """Tests for OpenAI API key format validation."""

    def test_valid_standard_api_key(self):
        """Should accept standard sk- prefixed API keys."""
        valid, error = _validate_api_key("openai", "sk-proj-" + "a" * 50)
        assert valid is True
        assert error == ""

    def test_valid_oauth_token(self):
        """Should accept long OAuth JWT tokens."""
        # OAuth tokens are 100+ chars, don't start with sk-
        long_token = "eyJhbGciOiJSUzI1NiIs" + "a" * 200
        valid, error = _validate_api_key("openai", long_token)
        assert valid is True

    def test_rejects_short_token(self):
        """Should reject tokens that are too short and don't match any pattern."""
        valid, error = _validate_api_key("openai", "short-key")
        assert valid is False
        assert "Invalid" in error

    def test_rejects_empty_key(self):
        """Should reject empty keys."""
        valid, error = _validate_api_key("openai", "")
        assert valid is False


class TestOpenAIProviderValidation:
    """Tests for provider validation with OpenAI."""

    def test_openai_recognized(self):
        """Should recognize 'openai' as a valid provider."""
        # Enable openai temporarily
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = True
        try:
            provider, err = _validate_provider("openai")
            assert provider == "openai"
            assert err is None
        finally:
            MODELS["openai"]["enabled"] = original

    def test_openai_disabled_error(self):
        """Should return error when openai is disabled."""
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = False
        try:
            provider, err = _validate_provider("openai")
            assert err is not None
            assert "disabled" in err
        finally:
            MODELS["openai"]["enabled"] = original

    def test_validate_openai_models(self):
        """Should validate OpenAI model names."""
        assert _validate_model("openai", "gpt-5.3-codex") is None
        assert _validate_model("openai", "gpt-5.2-thinking") is None
        assert _validate_model("openai", "gpt-5.2-instant") is None
        assert _validate_model("openai", "gpt-5.2-codex") is None
        err = _validate_model("openai", "gpt-4o")
        assert err is not None
        assert "not available" in err


class TestDefaultReviewProvider:
    """Tests for _default_review_provider() selection logic."""

    def test_prefers_openai_when_enabled(self):
        """Should prefer openai when it's enabled."""
        originals = {p: MODELS[p]["enabled"] for p in MODELS}
        MODELS["openai"]["enabled"] = True
        MODELS["deepseek"]["enabled"] = True
        try:
            assert _default_review_provider() == "openai"
        finally:
            for p, v in originals.items():
                MODELS[p]["enabled"] = v

    def test_falls_back_to_deepseek(self):
        """Should fall back to deepseek when openai is disabled."""
        originals = {p: MODELS[p]["enabled"] for p in MODELS}
        MODELS["openai"]["enabled"] = False
        MODELS["deepseek"]["enabled"] = True
        try:
            assert _default_review_provider() == "deepseek"
        finally:
            for p, v in originals.items():
                MODELS[p]["enabled"] = v

    def test_falls_back_to_moonshot(self):
        """Should fall back to moonshot when openai and deepseek are disabled."""
        originals = {p: MODELS[p]["enabled"] for p in MODELS}
        MODELS["openai"]["enabled"] = False
        MODELS["deepseek"]["enabled"] = False
        MODELS["moonshot"]["enabled"] = True
        try:
            assert _default_review_provider() == "moonshot"
        finally:
            for p, v in originals.items():
                MODELS[p]["enabled"] = v


class TestOpenAIInProvidersList:
    """Tests for OpenAI in SECOND_OPINION_PROVIDERS env var."""

    def test_accepts_openai_in_providers(self):
        """Should accept openai as valid provider name."""
        from second_opinion_mcp.server import _validate_configuration
        with patch.dict(os.environ, {"SECOND_OPINION_PROVIDERS": "deepseek,openai"}, clear=False):
            # Should not raise
            _validate_configuration()


class TestArchitectReviewValidation:
    """Tests for architect_review input validation."""

    @pytest.mark.asyncio
    async def test_rejects_null_byte_plan(self):
        """Should reject plan with null bytes."""
        from second_opinion_mcp.server import architect_review
        result = await architect_review(plan="valid plan\x00malicious")
        assert "Error" in result

    @pytest.mark.asyncio
    async def test_rejects_null_byte_context(self):
        """Should reject context with null bytes."""
        from second_opinion_mcp.server import architect_review
        result = await architect_review(plan="valid plan", context="ctx\x00evil")
        assert "Error" in result

    @pytest.mark.asyncio
    async def test_rejects_disabled_provider(self):
        """Should reject request when specified provider is disabled."""
        from second_opinion_mcp.server import architect_review
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = False
        try:
            result = await architect_review(plan="test plan", provider="openai")
            assert "Error" in result
            assert "disabled" in result
        finally:
            MODELS["openai"]["enabled"] = original

    @pytest.mark.asyncio
    async def test_rejects_invalid_model(self):
        """Should reject invalid model for provider."""
        from second_opinion_mcp.server import architect_review
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = True
        try:
            result = await architect_review(plan="test plan", provider="openai", model="nonexistent-model")
            assert "Error" in result
            assert "not available" in result
        finally:
            MODELS["openai"]["enabled"] = original


class TestImplementationReviewValidation:
    """Tests for implementation_review input validation."""

    @pytest.mark.asyncio
    async def test_rejects_nonexistent_directory(self):
        """Should reject non-existent directory."""
        from second_opinion_mcp.server import implementation_review
        result = await implementation_review(directory="/nonexistent/path/12345")
        assert "Error" in result
        assert "does not exist" in result

    @pytest.mark.asyncio
    async def test_rejects_file_path(self, tmp_path):
        """Should reject a file path (not directory)."""
        from second_opinion_mcp.server import implementation_review
        test_file = tmp_path / "test.py"
        test_file.write_text("print('hello')")
        result = await implementation_review(directory=str(test_file))
        assert "Error" in result
        assert "not a directory" in result

    @pytest.mark.asyncio
    async def test_rejects_null_byte_path(self):
        """Should reject path with null bytes."""
        from second_opinion_mcp.server import implementation_review
        result = await implementation_review(directory="/valid/path\x00/evil")
        assert "Error" in result

    @pytest.mark.asyncio
    async def test_rejects_blocked_system_path(self):
        """Should reject system directories."""
        from second_opinion_mcp.server import implementation_review, MODELS
        # Need at least one enabled provider
        original = MODELS["deepseek"]["enabled"]
        MODELS["deepseek"]["enabled"] = True
        try:
            result = await implementation_review(directory="/etc")
            assert "Error" in result
        finally:
            MODELS["deepseek"]["enabled"] = original


class TestIsOpenAIOAuth:
    """Tests for _is_openai_oauth() detection."""

    def test_returns_true_when_oauth_token_exists(self):
        """Should return True when get_openai_token() returns a token."""
        with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value="eyJ.fake.token"):
            assert _is_openai_oauth() is True

    def test_returns_false_when_no_oauth_token(self):
        """Should return False when get_openai_token() returns None."""
        with patch("second_opinion_mcp.openai_auth.get_openai_token", return_value=None):
            assert _is_openai_oauth() is False


class TestOAuthProviderRouting:
    """Tests for OAuth-based routing in call_model()."""

    @pytest.mark.asyncio
    async def test_call_model_routes_to_responses_api_when_oauth(self):
        """call_model should use _call_openai_responses_streaming when OAuth active."""
        from second_opinion_mcp.server import call_model, _oauth_providers

        _oauth_providers.add("openai")
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = True
        try:
            with patch("second_opinion_mcp.server.get_client"):
                with patch("second_opinion_mcp.server._call_openai_responses_streaming",
                           new_callable=AsyncMock, return_value=("response text", "")) as mock_fn:
                    result = await call_model("openai", "test prompt", system="sys")
                    mock_fn.assert_called_once()
                    assert result == "response text"
                    call_kwargs = mock_fn.call_args
                    assert call_kwargs.kwargs.get("model") == ""
        finally:
            MODELS["openai"]["enabled"] = original
            _oauth_providers.discard("openai")

    @pytest.mark.asyncio
    async def test_call_model_uses_standard_api_when_not_oauth(self):
        """call_model should use standard path when OpenAI is not in _oauth_providers."""
        from second_opinion_mcp.server import call_model, _oauth_providers

        _oauth_providers.discard("openai")
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = True
        try:
            with patch("second_opinion_mcp.server.get_client"):
                with patch("second_opinion_mcp.server._call_model_sync",
                           new_callable=AsyncMock, return_value=("sync response", "")) as mock_fn:
                    result = await call_model("openai", "test prompt")
                    mock_fn.assert_called_once()
                    assert result == "sync response"
        finally:
            MODELS["openai"]["enabled"] = original

    @pytest.mark.asyncio
    async def test_call_model_respects_explicit_model_override(self):
        """call_model should pass explicit model to responses API even with OAuth."""
        from second_opinion_mcp.server import call_model, _oauth_providers

        _oauth_providers.add("openai")
        original = MODELS["openai"]["enabled"]
        MODELS["openai"]["enabled"] = True
        try:
            with patch("second_opinion_mcp.server.get_client"):
                with patch("second_opinion_mcp.server._call_openai_responses_streaming",
                           new_callable=AsyncMock, return_value=("response", "")) as mock_fn:
                    await call_model("openai", "test", model="gpt-5.2-codex")
                    call_kwargs = mock_fn.call_args
                    assert call_kwargs.kwargs.get("model") == "gpt-5.2-codex"
        finally:
            MODELS["openai"]["enabled"] = original
            _oauth_providers.discard("openai")


class TestResolveReviewModelOAuth:
    """Tests for _resolve_review_model() with OAuth."""

    def test_returns_oauth_model_when_oauth_active(self):
        """Should return oauth_model when OpenAI is in _oauth_providers."""
        _oauth_providers.add("openai")
        try:
            result = _resolve_review_model("openai", "")
            assert result == "gpt-5.3-codex"
        finally:
            _oauth_providers.discard("openai")

    def test_returns_review_model_when_not_oauth(self):
        """Should return _REVIEW_MODELS entry when not using OAuth."""
        _oauth_providers.discard("openai")
        result = _resolve_review_model("openai", "")
        assert result == "gpt-5.3-codex"  # Updated default in _REVIEW_MODELS

    def test_explicit_model_overrides_oauth(self):
        """Explicit model should override OAuth model selection."""
        _oauth_providers.add("openai")
        try:
            result = _resolve_review_model("openai", "gpt-5.2-instant")
            assert result == "gpt-5.2-instant"
        finally:
            _oauth_providers.discard("openai")

    def test_deepseek_unaffected(self):
        """DeepSeek review model should be unaffected by OAuth logic."""
        result = _resolve_review_model("deepseek", "")
        assert result == "deepseek-reasoner"


class TestGetClientOAuth:
    """Tests for get_client() OAuth base URL selection."""

    def test_oauth_sets_chatgpt_base_url(self):
        """get_client should use oauth_base_url when OAuth is detected."""
        from second_opinion_mcp.server import _clients, _oauth_providers

        # Clean state
        _clients.pop("openai", None)
        _oauth_providers.discard("openai")

        with patch("second_opinion_mcp.server._is_openai_oauth", return_value=True), \
             patch("second_opinion_mcp.server.get_key", return_value="eyJ.fake.token"):
            from second_opinion_mcp.server import get_client
            client = get_client("openai")
            assert "openai" in _oauth_providers
            assert str(client.base_url).rstrip("/") == "https://chatgpt.com/backend-api/codex"

        # Cleanup
        _clients.pop("openai", None)
        _oauth_providers.discard("openai")

    def test_api_key_uses_standard_base_url(self):
        """get_client should use standard base_url when not OAuth."""
        from second_opinion_mcp.server import _clients, _oauth_providers

        # Clean state
        _clients.pop("openai", None)
        _oauth_providers.discard("openai")

        with patch("second_opinion_mcp.server._is_openai_oauth", return_value=False), \
             patch("second_opinion_mcp.server.get_key", return_value="sk-proj-" + "a" * 50):
            from second_opinion_mcp.server import get_client
            client = get_client("openai")
            assert "openai" not in _oauth_providers
            assert "api.openai.com" in str(client.base_url)

        # Cleanup
        _clients.pop("openai", None)
        _oauth_providers.discard("openai")


class TestCleanupClearsOAuthProviders:
    """Tests for cleanup_clients() clearing _oauth_providers."""

    @pytest.mark.asyncio
    async def test_cleanup_clears_oauth_set(self):
        """cleanup_clients should clear _oauth_providers."""
        from second_opinion_mcp.server import cleanup_clients, _oauth_providers
        _oauth_providers.add("openai")
        await cleanup_clients()
        assert len(_oauth_providers) == 0
