"""
Unit tests for fabias.core module using mocked HTTP calls.

Tests authentication, exceptions, client utilities, and helper functions.
"""

from unittest.mock import Mock

import pytest
import responses

from fabias._shared.auth import RefreshTokenAuth, ServicePrincipalAuth
from fabias._shared.exceptions import (
    ApiError,
    AuthenticationError,
    FabiasError,
    NotFoundError,
    OperationError,
)
from fabias._shared.utils import GUID, sync_delay


class TestExceptions:
    """Test custom exception classes"""

    def test_fabias_error_basic(self):
        """Test FabiasError base exception"""
        error = FabiasError("Something went wrong")
        assert str(error) == "Something went wrong"
        assert isinstance(error, Exception)

    def test_fabias_error_with_cause(self):
        """Test FabiasError with exception chaining"""
        original = ValueError("Original error")
        error = FabiasError("Wrapped error", cause=original)
        assert "Wrapped error" in str(error)
        assert "ValueError" in str(error)
        assert error.cause == original

    def test_api_error_with_details(self):
        """Test ApiError with status code and response body"""
        error = ApiError("Request failed", status_code=400, response_body={"error": "Bad request"})
        assert error.status_code == 400
        assert error.response_body == {"error": "Bad request"}
        assert "Request failed" in str(error)

    def test_authentication_error(self):
        """Test AuthenticationError"""
        error = AuthenticationError("Invalid credentials")
        assert isinstance(error, FabiasError)
        assert "Invalid credentials" in str(error)

    def test_not_found_error(self):
        """Test NotFoundError is subclass of ApiError"""
        error = NotFoundError("Resource not found")
        assert isinstance(error, ApiError)
        assert isinstance(error, FabiasError)
        assert "Resource not found" in str(error)

    def test_operation_error_with_details(self):
        """Test OperationError with operation details"""
        error = OperationError("Operation failed", operation_id="op-123", operation_status="failed")
        assert error.operation_id == "op-123"
        assert error.operation_status == "failed"

    def test_validation_error_compatibility(self):
        """Test creating error for validation scenarios"""
        # Even without ValidationError, we can use FabiasError
        error = FabiasError("Invalid value for field 'email'")
        assert "Invalid value" in str(error)


class TestServicePrincipalAuth:
    """Test ServicePrincipalAuth authentication provider"""

    @responses.activate
    def test_get_token_success(self):
        """Test successful token acquisition"""
        # Mock token endpoint
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={"access_token": "test-access-token", "expires_in": 3600, "token_type": "Bearer"},
            status=200,
        )

        auth = ServicePrincipalAuth(
            tenant_id="test-tenant", client_id="test-client", client_secret="test-secret"
        )

        token = auth.getToken("https://management.azure.com/.default")
        assert token == "test-access-token"

    @responses.activate
    def test_get_token_uses_cache(self):
        """Test token is cached and reused"""
        # Mock token endpoint
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={"access_token": "cached-token", "expires_in": 3600},
            status=200,
        )

        auth = ServicePrincipalAuth(
            tenant_id="test-tenant", client_id="test-client", client_secret="test-secret"
        )

        # First call - hits API
        token1 = auth.getToken("https://management.azure.com/.default")
        assert len(responses.calls) == 1

        # Second call - uses cache
        token2 = auth.getToken("https://management.azure.com/.default")
        assert len(responses.calls) == 1  # No new API call
        assert token1 == token2

    @responses.activate
    def test_get_token_failure(self):
        """Test token acquisition failure"""
        # Mock token endpoint failure
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={"error": "invalid_client", "error_description": "Invalid credentials"},
            status=401,
        )

        auth = ServicePrincipalAuth(
            tenant_id="test-tenant", client_id="bad-client", client_secret="bad-secret"
        )

        with pytest.raises(AuthenticationError, match="Token acquisition failed"):
            auth.getToken("https://management.azure.com/.default")

    def test_get_authorization_header(self):
        """Test authorization header format"""
        auth = ServicePrincipalAuth(
            tenant_id="test-tenant", client_id="test-client", client_secret="test-secret"
        )

        # Mock getToken to avoid actual API call
        auth.getToken = Mock(return_value="test-token-123")

        header = auth.getAuthorizationHeader("test-scope")
        assert header == "Bearer test-token-123"


class TestRefreshTokenAuth:
    """Test RefreshTokenAuth authentication provider"""

    @responses.activate
    def test_get_token_self_managed_success(self):
        """Test successful token acquisition with refresh token"""
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={
                "access_token": "new-access-token",
                "refresh_token": "new-refresh-token",
                "expires_in": 3600,
            },
            status=200,
        )

        on_refresh_called = []

        auth = RefreshTokenAuth(
            tenant_id="test-tenant",
            client_id="test-client",
            refresh_token="initial-refresh-token",
            on_refresh=lambda t: on_refresh_called.append(t),
        )

        token = auth.getToken("https://graph.microsoft.com/.default")
        assert token == "new-access-token"
        assert auth.refresh_token == "new-refresh-token"
        assert on_refresh_called == ["new-refresh-token"]

    @responses.activate
    def test_get_token_uses_cache(self):
        """Test token is cached and reused"""
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={
                "access_token": "cached-token",
                "refresh_token": "new-refresh",
                "expires_in": 3600,
            },
            status=200,
        )

        auth = RefreshTokenAuth(
            tenant_id="test-tenant",
            client_id="test-client",
            refresh_token="initial-token",
        )

        token1 = auth.getToken("https://graph.microsoft.com/.default")
        assert len(responses.calls) == 1

        token2 = auth.getToken("https://graph.microsoft.com/.default")
        assert len(responses.calls) == 1  # No new API call - cached
        assert token1 == token2

    def test_token_provider_delegated_mode(self):
        """Test delegated mode with external token provider"""
        from datetime import datetime, timedelta, timezone

        future_expiry = datetime.now(timezone.utc) + timedelta(hours=1)
        provider_calls = []

        def mock_provider(scope: str) -> tuple:
            provider_calls.append(scope)
            return ("broker-token", future_expiry)

        auth = RefreshTokenAuth(
            tenant_id="test-tenant",
            client_id="test-client",
            token_provider=mock_provider,
        )

        token = auth.getToken("https://graph.microsoft.com/.default")
        assert token == "broker-token"
        assert provider_calls == ["https://graph.microsoft.com/.default"]

    def test_requires_either_refresh_token_or_provider(self):
        """Test ValueError when neither refresh_token nor token_provider provided"""
        with pytest.raises(ValueError, match="Either refresh_token.*or token_provider"):
            RefreshTokenAuth(
                tenant_id="test-tenant",
                client_id="test-client",
            )

    @responses.activate
    def test_invalid_grant_error(self):
        """Test handling of invalid_grant error (token rotated by another process)"""
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={
                "error": "invalid_grant",
                "error_description": "Refresh token has expired",
            },
            status=400,
        )

        auth = RefreshTokenAuth(
            tenant_id="test-tenant",
            client_id="test-client",
            refresh_token="stale-token",
        )

        with pytest.raises(AuthenticationError, match="invalid or expired"):
            auth.getToken("https://graph.microsoft.com/.default")

    @responses.activate
    def test_on_refresh_failure_logged_but_not_raised(self):
        """Test that on_refresh callback failure doesn't fail the auth"""
        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={
                "access_token": "new-access-token",
                "refresh_token": "new-refresh-token",
                "expires_in": 3600,
            },
            status=200,
        )

        def failing_callback(_):
            raise Exception("KeyVault write failed")

        auth = RefreshTokenAuth(
            tenant_id="test-tenant",
            client_id="test-client",
            refresh_token="initial-token",
            on_refresh=failing_callback,
        )

        # Should still succeed even though callback failed
        token = auth.getToken("https://graph.microsoft.com/.default")
        assert token == "new-access-token"


class TestUtils:
    """Test utility functions"""

    def test_guid_valid_true(self):
        """Test GUID.valid() with valid GUIDs"""
        assert GUID.valid("12345678-1234-1234-1234-123456789abc") is True
        assert GUID.valid("ABCDEF00-ABCD-ABCD-ABCD-ABCDEFABCDEF") is True
        assert GUID.valid("00000000-0000-0000-0000-000000000000") is True

    def test_guid_valid_false(self):
        """Test GUID.valid() with invalid values"""
        assert GUID.valid("not-a-guid") is False
        assert GUID.valid("12345") is False
        assert GUID.valid(None) is False
        assert GUID.valid("") is False
        assert GUID.valid("1234567812341234123412345678abcd") is False  # No dashes

    def test_guid_new(self):
        """Test GUID.new() generates valid GUID"""
        guid = GUID.new()
        assert GUID.valid(guid) is True
        assert isinstance(guid, str)
        assert len(guid) == 36  # 32 chars + 4 dashes

    def test_guid_hex(self):
        """Test GUID.hex() generates hex string"""
        hex_guid = GUID.hex()
        assert isinstance(hex_guid, str)
        assert len(hex_guid) == 32  # No dashes
        assert all(c in "0123456789abcdef" for c in hex_guid.lower())

    def test_sync_delay(self):
        """Test sync_delay function"""
        import time

        start = time.time()
        sync_delay(0.1)
        elapsed = time.time() - start
        assert elapsed >= 0.1
        assert elapsed < 0.2  # Should not take too long


class TestClientErrorHandling:
    """Test client error handling edge cases"""

    @responses.activate
    def test_error_response_without_json(self):
        """Test handling error response with no JSON body"""
        from fabias._shared.auth import ServicePrincipalAuth
        from fabias._shared.client import BaseClient

        responses.add(
            responses.POST,
            "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
            json={"access_token": "test-token", "expires_in": 3600},
            status=200,
        )

        responses.add(
            responses.GET, "https://api.test.com/resource", body="Plain text error", status=500
        )

        auth = ServicePrincipalAuth("test-tenant", "test-client", "test-secret")
        client = BaseClient(auth)
        client._base_uri = "https://api.test.com"
        client._scope = "test-scope"

        with pytest.raises(ApiError, match="Plain text error"):
            client.get("/resource")
