"""
Tests for CloudBridge OAuth credential resolution.

Verifies the credential priority chain:
    1. OAuth token (preferred when available)
    2. BYOK API key (fallback)
    3. Error when neither available

Also tests backend-specific auth header formatting for each provider.
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from runtime.executor.cloud_bridge import (
    CloudBridge,
    Credential,
    NoCredentialsError,
    _call_claude,
    _call_gemini,
    _call_gpt,
)


# ---------------------------------------------------------------------------
# Test helpers / fakes
# ---------------------------------------------------------------------------


class FakeOAuthManager:
    """Fake OAuthManager that returns pre-configured tokens."""

    def __init__(self, tokens: dict[str, str | None] | None = None) -> None:
        self._tokens = tokens or {}

    async def get_token(self, provider: str) -> Optional[str]:
        return self._tokens.get(provider)


class FakeKeyVault:
    """Fake KeyVault that returns pre-configured API keys."""

    def __init__(self, keys: dict[str, str | None] | None = None) -> None:
        self._keys = keys or {}

    def get_key(self, provider: str) -> Optional[str]:
        return self._keys.get(provider)


class FailingOAuthManager:
    """OAuthManager that always raises on get_token."""

    async def get_token(self, provider: str) -> Optional[str]:
        raise RuntimeError("OAuth store corrupted")


class FailingKeyVault:
    """KeyVault that always raises on get_key."""

    def get_key(self, provider: str) -> Optional[str]:
        raise RuntimeError("Vault decryption failed")


# ---------------------------------------------------------------------------
# Credential resolution tests
# ---------------------------------------------------------------------------


class TestGetCredential:
    """Tests for CloudBridge._get_credential() priority chain."""

    @pytest.mark.asyncio
    async def test_oauth_token_preferred_over_api_key(self) -> None:
        """OAuth token should be used when both OAuth and API key are available."""
        oauth = FakeOAuthManager({"anthropic": "oauth-token-123"})
        vault = FakeKeyVault({"anthropic": "sk-ant-api-key-456"})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        cred = await bridge._get_credential("claude")

        assert cred.type == "oauth"
        assert cred.value == "oauth-token-123"
        assert cred.provider == "anthropic"

    @pytest.mark.asyncio
    async def test_falls_back_to_api_key_when_no_oauth(self) -> None:
        """Should fall back to API key when OAuth returns None."""
        oauth = FakeOAuthManager({})  # No tokens
        vault = FakeKeyVault({"anthropic": "sk-ant-api-key-456"})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        cred = await bridge._get_credential("claude")

        assert cred.type == "api_key"
        assert cred.value == "sk-ant-api-key-456"
        assert cred.provider == "anthropic"

    @pytest.mark.asyncio
    async def test_error_when_neither_available(self) -> None:
        """Should raise NoCredentialsError when no OAuth token or API key."""
        oauth = FakeOAuthManager({})
        vault = FakeKeyVault({})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        with pytest.raises(NoCredentialsError, match="No credentials available"):
            await bridge._get_credential("claude")

    @pytest.mark.asyncio
    async def test_error_message_includes_provider_name(self) -> None:
        """Error message should mention the friendly provider name."""
        oauth = FakeOAuthManager({})
        vault = FakeKeyVault({})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        with pytest.raises(NoCredentialsError, match="Claude"):
            await bridge._get_credential("claude")

        with pytest.raises(NoCredentialsError, match="Gemini"):
            await bridge._get_credential("gemini")

        with pytest.raises(NoCredentialsError, match="ChatGPT"):
            await bridge._get_credential("gpt")

    @pytest.mark.asyncio
    async def test_falls_back_to_api_key_when_oauth_errors(self) -> None:
        """Should fall back to API key if OAuth manager throws."""
        oauth = FailingOAuthManager()
        vault = FakeKeyVault({"google": "AIza-gemini-key"})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        cred = await bridge._get_credential("gemini")

        assert cred.type == "api_key"
        assert cred.value == "AIza-gemini-key"

    @pytest.mark.asyncio
    async def test_error_when_both_sources_fail(self) -> None:
        """Should raise NoCredentialsError when both OAuth and vault error."""
        oauth = FailingOAuthManager()
        vault = FailingKeyVault()
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        with pytest.raises(NoCredentialsError):
            await bridge._get_credential("gpt")

    @pytest.mark.asyncio
    async def test_no_oauth_manager_skips_to_api_key(self) -> None:
        """Should skip OAuth check entirely if no OAuthManager provided."""
        vault = FakeKeyVault({"openai": "sk-openai-key"})
        bridge = CloudBridge(oauth_manager=None, key_vault=vault)

        cred = await bridge._get_credential("gpt")

        assert cred.type == "api_key"
        assert cred.value == "sk-openai-key"

    @pytest.mark.asyncio
    async def test_no_vault_skips_to_error(self) -> None:
        """Should fail if OAuth returns None and no vault provided."""
        oauth = FakeOAuthManager({})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=None)

        with pytest.raises(NoCredentialsError):
            await bridge._get_credential("claude")

    @pytest.mark.asyncio
    async def test_backend_alias_openai(self) -> None:
        """Backend 'openai' should resolve to provider 'openai' same as 'gpt'."""
        oauth = FakeOAuthManager({"openai": "oauth-openai-tok"})
        bridge = CloudBridge(oauth_manager=oauth)

        cred = await bridge._get_credential("openai")

        assert cred.provider == "openai"
        assert cred.value == "oauth-openai-tok"


# ---------------------------------------------------------------------------
# Claude OAuth header format tests
# ---------------------------------------------------------------------------


class TestClaudeOAuthHeaders:
    """Verify Claude uses correct auth headers for OAuth vs API key."""

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_claude_oauth_uses_bearer_header(self, mock_http: MagicMock) -> None:
        """Claude with OAuth should use Authorization: Bearer, not x-api-key."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "Hello"}],
            "usage": {"input_tokens": 10, "output_tokens": 5},
        }

        cred = Credential(type="oauth", value="oauth-tok-abc", provider="anthropic")
        result = _call_claude("claude-sonnet-4-20250514", "Hi", cred, {})

        assert result.success is True
        call_kwargs = mock_http.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert "Authorization" in headers
        assert headers["Authorization"] == "Bearer oauth-tok-abc"
        assert "x-api-key" not in headers

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_claude_api_key_uses_x_api_key_header(self, mock_http: MagicMock) -> None:
        """Claude with API key should use x-api-key, not Authorization."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "Hello"}],
            "usage": {"input_tokens": 10, "output_tokens": 5},
        }

        cred = Credential(type="api_key", value="sk-ant-key", provider="anthropic")
        result = _call_claude("claude-sonnet-4-20250514", "Hi", cred, {})

        assert result.success is True
        call_kwargs = mock_http.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert "x-api-key" in headers
        assert headers["x-api-key"] == "sk-ant-key"
        assert "Authorization" not in headers

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_claude_oauth_includes_auth_type_metadata(self, mock_http: MagicMock) -> None:
        """Response metadata should indicate 'oauth' auth type."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "Hi"}],
            "usage": {},
        }

        cred = Credential(type="oauth", value="tok", provider="anthropic")
        result = _call_claude("claude-sonnet-4-20250514", "Hi", cred, {})

        assert result.metadata.get("auth_type") == "oauth"


# ---------------------------------------------------------------------------
# Gemini OAuth format tests
# ---------------------------------------------------------------------------


class TestGeminiOAuthFormat:
    """Verify Gemini uses the same key= parameter for both OAuth and API key."""

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_gemini_oauth_uses_bearer_header(self, mock_http: MagicMock) -> None:
        """Gemini OAuth token goes in Authorization: Bearer header (not ?key=)."""
        mock_http.return_value = {
            "candidates": [
                {"content": {"parts": [{"text": "Hello from Gemini"}]}}
            ],
            "usageMetadata": {"promptTokenCount": 5, "candidatesTokenCount": 4},
        }

        cred = Credential(type="oauth", value="google-oauth-tok", provider="google")
        result = _call_gemini("gemini-2.0-flash", "Hi", cred, {})

        assert result.success is True
        call_args = mock_http.call_args
        headers = call_args.kwargs.get("headers") or (call_args[1].get("headers") if len(call_args) > 1 else {})
        assert headers.get("Authorization") == "Bearer google-oauth-tok"

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_gemini_api_key_uses_key_parameter(self, mock_http: MagicMock) -> None:
        """Gemini API key also goes in the ?key= query param."""
        mock_http.return_value = {
            "candidates": [
                {"content": {"parts": [{"text": "Hello"}]}}
            ],
            "usageMetadata": {},
        }

        cred = Credential(type="api_key", value="AIza-key-123", provider="google")
        result = _call_gemini("gemini-2.0-flash", "Hi", cred, {})

        assert result.success is True
        call_args = mock_http.call_args
        url = call_args[0][1] if len(call_args[0]) > 1 else call_args.kwargs.get("url", "")
        assert "key=AIza-key-123" in url


# ---------------------------------------------------------------------------
# GPT OAuth format tests
# ---------------------------------------------------------------------------


class TestGptOAuthFormat:
    """Verify GPT uses Bearer auth for both OAuth and API key."""

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_gpt_oauth_uses_bearer(self, mock_http: MagicMock) -> None:
        """GPT OAuth should use Authorization: Bearer (same format as API key)."""
        mock_http.return_value = {
            "choices": [{"message": {"content": "Hello from GPT"}}],
            "usage": {"prompt_tokens": 5, "completion_tokens": 4},
        }

        cred = Credential(type="oauth", value="openai-oauth-tok", provider="openai")
        result = _call_gpt("gpt-4o", "Hi", cred, {})

        assert result.success is True
        call_kwargs = mock_http.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert headers["Authorization"] == "Bearer openai-oauth-tok"

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_gpt_api_key_uses_bearer(self, mock_http: MagicMock) -> None:
        """GPT API key also uses Authorization: Bearer."""
        mock_http.return_value = {
            "choices": [{"message": {"content": "Hi"}}],
            "usage": {},
        }

        cred = Credential(type="api_key", value="sk-openai-key", provider="openai")
        result = _call_gpt("gpt-4o", "Hi", cred, {})

        assert result.success is True
        call_kwargs = mock_http.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert headers["Authorization"] == "Bearer sk-openai-key"


# ---------------------------------------------------------------------------
# call_with_resolution integration tests
# ---------------------------------------------------------------------------


class TestCallWithResolution:
    """Tests for the high-level call_with_resolution() method."""

    @pytest.mark.asyncio
    @patch("runtime.executor.cloud_bridge._http_json")
    async def test_resolves_oauth_and_calls_claude(self, mock_http: MagicMock) -> None:
        """call_with_resolution should resolve OAuth and make the API call."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "OAuth response"}],
            "usage": {"input_tokens": 10, "output_tokens": 20},
        }

        oauth = FakeOAuthManager({"anthropic": "my-oauth-token"})
        bridge = CloudBridge(oauth_manager=oauth)

        result = await bridge.call_with_resolution(
            "claude", "claude-sonnet-4-20250514", "Hello", {}
        )

        assert result.success is True
        assert result.output == "OAuth response"
        assert result.metadata.get("auth_type") == "oauth"

    @pytest.mark.asyncio
    @patch("runtime.executor.cloud_bridge._http_json")
    async def test_resolves_api_key_fallback(self, mock_http: MagicMock) -> None:
        """call_with_resolution should fall back to API key."""
        mock_http.return_value = {
            "choices": [{"message": {"content": "API key response"}}],
            "usage": {"prompt_tokens": 5, "completion_tokens": 10},
        }

        oauth = FakeOAuthManager({})  # No tokens
        vault = FakeKeyVault({"openai": "sk-test-key"})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        result = await bridge.call_with_resolution(
            "gpt", "gpt-4o", "Hello", {}
        )

        assert result.success is True
        assert result.output == "API key response"
        assert result.metadata.get("auth_type") == "api_key"

    @pytest.mark.asyncio
    async def test_returns_error_when_no_credentials(self) -> None:
        """call_with_resolution should return error BridgeResponse, not raise."""
        oauth = FakeOAuthManager({})
        vault = FakeKeyVault({})
        bridge = CloudBridge(oauth_manager=oauth, key_vault=vault)

        result = await bridge.call_with_resolution(
            "claude", "claude-sonnet-4-20250514", "Hello", {}
        )

        assert result.success is False
        assert "No credentials available" in (result.error or "")
        assert result.backend == "claude"

    @pytest.mark.asyncio
    async def test_unknown_backend_returns_error(self) -> None:
        """call_with_resolution should handle unknown backends gracefully."""
        bridge = CloudBridge()

        result = await bridge.call_with_resolution(
            "unknown_llm", "some-model", "Hello", {}
        )

        assert result.success is False
        assert "Unknown cloud backend" in (result.error or "")


# ---------------------------------------------------------------------------
# Backward compatibility tests
# ---------------------------------------------------------------------------


class TestBackwardCompatibility:
    """Ensure the old call() method still works with explicit API keys."""

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_call_with_explicit_api_key(self, mock_http: MagicMock) -> None:
        """The original call() method should still work with a raw API key."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "Legacy response"}],
            "usage": {"input_tokens": 10, "output_tokens": 5},
        }

        bridge = CloudBridge()
        result = bridge.call(
            "claude", "claude-sonnet-4-20250514", "Hello", "sk-ant-key-123", {}
        )

        assert result.success is True
        assert result.output == "Legacy response"
        assert result.metadata.get("auth_type") == "api_key"

    @patch("runtime.executor.cloud_bridge._http_json")
    def test_call_streaming_still_works(self, mock_http: MagicMock) -> None:
        """call_streaming() should still work with explicit API key."""
        mock_http.return_value = {
            "content": [{"type": "text", "text": "Streamed output"}],
            "usage": {},
        }

        bridge = CloudBridge()
        chunks = list(bridge.call_streaming(
            "claude", "claude-sonnet-4-20250514", "Hello", "sk-ant-key"
        ))

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