"""
Unit tests for the GAMClient class.

These tests use mocking to avoid actual network calls and test
all functionality in isolation.
"""

import pytest
from unittest.mock import Mock, patch
import requests

from pygeai_gam import (
    GAMClient,
    GAMAuthenticationError,
    GAMAPIError,
    GAMInvalidResponseError,
    GAMMissingParameterError,
)


class TestGAMClientInit:
    """Test GAMClient initialization."""

    def test_init_with_base_url_success(self):
        """Test successful initialization with base_url."""
        client = GAMClient(base_url="https://test.com")
        assert client.base_url == "https://test.com"
        assert client.timeout == 30
        assert client.verify_ssl is True

    def test_init_with_custom_timeout(self):
        """Test initialization with custom timeout."""
        client = GAMClient(base_url="https://test.com", timeout=60)
        assert client.timeout == 60

    def test_init_with_verify_ssl_false(self):
        """Test initialization with SSL verification disabled."""
        client = GAMClient(base_url="https://test.com", verify_ssl=False)
        assert client.verify_ssl is False

    def test_init_strips_trailing_slash(self):
        """Test that trailing slash is removed from base_url."""
        client = GAMClient(base_url="https://test.com/")
        assert client.base_url == "https://test.com"

    def test_init_without_base_url_raises_error(self):
        """Test that initialization without base_url raises error."""
        with pytest.raises(GAMMissingParameterError) as exc_info:
            GAMClient(base_url="")
        assert "base_url" in str(exc_info.value)


class TestGenerateSigningUrl:
    """Test generate_signing_url method."""

    def test_generate_signing_url_success(self):
        """Test successful URL generation with all parameters."""
        client = GAMClient(base_url="https://gam.test.com")
        url = client.generate_signing_url(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="random_state",
            scope="gam_user_data",
        )

        assert url.startswith("https://gam.test.com/oauth/gam/signin?")
        assert "client_id=test_client" in url
        assert "redirect_uri=https%3A%2F%2Fapp.com%2Fcallback" in url
        assert "state=random_state" in url
        assert "scope=gam_user_data" in url
        assert "response_type=code" in url

    def test_generate_signing_url_with_custom_scope(self):
        """Test URL generation with custom scope."""
        client = GAMClient(base_url="https://gam.test.com")
        url = client.generate_signing_url(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
            scope="gam_user_data+gam_user_roles",
        )

        assert "scope=gam_user_data%2Bgam_user_roles" in url

    def test_generate_signing_url_missing_client_id_raises_error(self):
        """Test that missing client_id raises error."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError) as exc_info:
            client.generate_signing_url(
                client_id=None,
                redirect_uri="https://app.com/callback",
                state="state123",
            )
        assert "client_id" in str(exc_info.value)

    def test_generate_signing_url_missing_redirect_uri_raises_error(self):
        """Test that missing redirect_uri raises error."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError) as exc_info:
            client.generate_signing_url(
                client_id="test_client",
                redirect_uri=None,
                state="state123",
            )
        assert "redirect_uri" in str(exc_info.value)

    def test_generate_signing_url_missing_state_raises_error(self):
        """Test that missing state raises error."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError) as exc_info:
            client.generate_signing_url(
                client_id="test_client",
                redirect_uri="https://app.com/callback",
                state=None,
            )
        assert "state" in str(exc_info.value)


class TestGetAccessToken:
    """Test get_access_token method."""

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_success(self, mock_request):
        """Test successful access token retrieval."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "access_token": "test_access_token",
            "token_type": "Bearer",
            "expires_in": 3600,
            "refresh_token": "test_refresh_token",
            "scope": "gam_user_data",
            "user_guid": "user123",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.get_access_token(
            username="testuser",
            password="testpass",
            scope="gam_user_data",
        )

        assert response["access_token"] == "test_access_token"
        assert response["refresh_token"] == "test_refresh_token"
        assert response["expires_in"] == 3600
        mock_request.assert_called_once()

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_with_all_params(self, mock_request):
        """Test access token retrieval with all parameters."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {"access_token": "token"}
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.get_access_token(
            client_id="client123",
            client_secret="secret456",
            username="user",
            password="pass",
            scope="gam_user_data+gam_user_roles",
            initial_properties={"key": "value"},
            repository="repo1",
        )

        assert response["access_token"] == "token"
        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["data"]["client_id"] == "client123"
        assert call_kwargs["data"]["client_secret"] == "secret456"
        assert call_kwargs["data"]["username"] == "user"
        assert call_kwargs["data"]["password"] == "pass"

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_401_raises_auth_error(self, mock_request):
        """Test that 401 status raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.ok = False
        mock_response.text = "Unauthorized"
        mock_response.json.return_value = {
            "error": "invalid_grant",
            "error_description": "Invalid credentials",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError) as exc_info:
            client.get_access_token(username="user", password="wrong")

        assert exc_info.value.status_code == 401
        assert "Invalid credentials" in str(exc_info.value)

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_403_raises_auth_error(self, mock_request):
        """Test that 403 status raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 403
        mock_response.ok = False
        mock_response.text = "Forbidden"
        mock_response.reason = "Forbidden"
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert exc_info.value.status_code == 403

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_500_raises_api_error(self, mock_request):
        """Test that 500 status raises GAMAPIError."""
        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.ok = False
        mock_response.text = "Internal Server Error"
        mock_response.reason = "Internal Server Error"
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert exc_info.value.status_code == 500

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_400_with_error_json_raises_api_error(self, mock_request):
        """Test that 400 status with error JSON raises GAMAPIError with error description."""
        mock_response = Mock()
        mock_response.status_code = 400
        mock_response.ok = False
        mock_response.json.return_value = {
            "error": "invalid_request",
            "error_description": "Missing required parameter: client_id",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert exc_info.value.status_code == 400
        assert "Missing required parameter: client_id" in str(exc_info.value)

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_invalid_json_raises_invalid_response_error(self, mock_request):
        """Test that invalid JSON raises GAMInvalidResponseError."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.text = "Not JSON"
        mock_response.json.side_effect = ValueError("Invalid JSON")
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMInvalidResponseError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert "Unable to parse" in str(exc_info.value)

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_timeout_raises_api_error(self, mock_request):
        """Test that timeout raises GAMAPIError."""
        mock_request.side_effect = requests.exceptions.Timeout("Request timeout")

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert "timeout" in str(exc_info.value).lower()

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_connection_error_raises_api_error(self, mock_request):
        """Test that connection error raises GAMAPIError."""
        mock_request.side_effect = requests.exceptions.ConnectionError("Connection failed")

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert "Connection error" in str(exc_info.value)

    @patch("pygeai_gam.client.requests.request")
    def test_get_access_token_generic_request_exception_raises_api_error(self, mock_request):
        """Test that generic RequestException raises GAMAPIError."""
        mock_request.side_effect = requests.exceptions.RequestException("Generic request error")

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError) as exc_info:
            client.get_access_token(username="user", password="pass")

        assert "Request failed" in str(exc_info.value)


class TestGetUserInfo:
    """Test get_user_info method."""

    @patch("pygeai_gam.client.requests.request")
    def test_get_user_info_success(self, mock_request):
        """Test successful user info retrieval."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "guid": "user123",
            "username": "testuser",
            "email": "test@example.com",
            "name": "Test User",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        user_info = client.get_user_info(access_token="test_token")

        assert user_info["guid"] == "user123"
        assert user_info["username"] == "testuser"
        assert user_info["email"] == "test@example.com"

        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["headers"]["Authorization"] == "test_token"

    @patch("pygeai_gam.client.requests.request")
    def test_get_user_info_missing_token_raises_error(self, mock_request):
        """Test that missing access_token raises error."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError) as exc_info:
            client.get_user_info(access_token="")

        assert "access_token" in str(exc_info.value)

    @patch("pygeai_gam.client.requests.request")
    def test_get_user_info_401_raises_auth_error(self, mock_request):
        """Test that 401 status raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.ok = False
        mock_response.text = "Invalid token"
        mock_response.reason = "Unauthorized"
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError) as exc_info:
            client.get_user_info(access_token="invalid_token")

        assert exc_info.value.status_code == 401

    @patch("pygeai_gam.client.requests.request")
    def test_get_user_info_invalid_json_raises_error(self, mock_request):
        """Test that invalid JSON raises GAMInvalidResponseError."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.text = "Invalid JSON"
        mock_response.json.side_effect = ValueError("Not JSON")
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMInvalidResponseError):
            client.get_user_info(access_token="test_token")


class TestRefreshAccessToken:
    """Test refresh_access_token method."""

    @patch("pygeai_gam.client.requests.request")
    def test_refresh_access_token_success(self, mock_request):
        """Test successful token refresh."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "access_token": "new_access_token",
            "refresh_token": "new_refresh_token",
            "expires_in": 3600,
            "token_type": "Bearer",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.refresh_access_token(
            client_id="client123",
            client_secret="secret456",
            refresh_token="old_refresh_token",
        )

        assert response["access_token"] == "new_access_token"
        assert response["refresh_token"] == "new_refresh_token"

        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["data"]["grant_type"] == "refresh_token"
        assert call_kwargs["data"]["refresh_token"] == "old_refresh_token"

    @patch("pygeai_gam.client.requests.request")
    def test_refresh_access_token_with_minimal_params(self, mock_request):
        """Test token refresh with minimal parameters."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {"access_token": "new_token"}
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.refresh_access_token(refresh_token="old_token")

        assert response["access_token"] == "new_token"

    @patch("pygeai_gam.client.requests.request")
    def test_refresh_access_token_401_raises_auth_error(self, mock_request):
        """Test that 401 status raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.ok = False
        mock_response.text = "Invalid refresh token"
        mock_response.reason = "Unauthorized"
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError):
            client.refresh_access_token(refresh_token="invalid_token")

    @patch("pygeai_gam.client.requests.request")
    def test_refresh_access_token_invalid_json_raises_error(self, mock_request):
        """Test that invalid JSON raises GAMInvalidResponseError."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.text = "Not JSON"
        mock_response.json.side_effect = ValueError("Invalid")
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMInvalidResponseError):
            client.refresh_access_token(refresh_token="token")


class TestGetAuthenticationTypes:
    """Test get_authentication_types method."""

    @patch("pygeai_gam.client.requests.request")
    def test_get_authentication_types_success(self, mock_request):
        """Test successful authentication types retrieval."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "authentication_types": [
                {"name": "local", "description": "Local authentication"},
                {"name": "oauth", "description": "OAuth authentication"},
            ]
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        auth_types = client.get_authentication_types()

        assert "authentication_types" in auth_types
        assert len(auth_types["authentication_types"]) == 2

    @patch("pygeai_gam.client.requests.request")
    def test_get_authentication_types_500_raises_api_error(self, mock_request):
        """Test that 500 status raises GAMAPIError."""
        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.ok = False
        mock_response.text = "Server error"
        mock_response.reason = "Internal Server Error"
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAPIError):
            client.get_authentication_types()

    @patch("pygeai_gam.client.requests.request")
    def test_get_authentication_types_invalid_json_raises_error(self, mock_request):
        """Test that invalid JSON raises GAMInvalidResponseError."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.text = "Not JSON"
        mock_response.json.side_effect = ValueError("Invalid")
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMInvalidResponseError):
            client.get_authentication_types()


class TestExceptions:
    """Test custom exceptions."""

    def test_gam_client_error_with_status_code(self):
        """Test GAMClientError with status code."""
        error = GAMAPIError("Test error", status_code=400)
        assert error.status_code == 400
        assert "[400]" in str(error)
        assert "Test error" in str(error)

    def test_gam_client_error_without_status_code(self):
        """Test GAMClientError without status code."""
        error = GAMAPIError("Test error")
        assert error.status_code is None
        assert str(error) == "Test error"

    def test_gam_missing_parameter_error_with_parameter_name(self):
        """Test GAMMissingParameterError with parameter name."""
        error = GAMMissingParameterError("Required parameter missing", parameter_name="username")
        assert error.parameter_name == "username"
        assert "Missing required parameter: username" in str(error)

    def test_gam_missing_parameter_error_without_parameter_name(self):
        """Test GAMMissingParameterError without parameter name."""
        error = GAMMissingParameterError("Some parameters missing")
        assert error.parameter_name is None
        assert str(error) == "Some parameters missing"


class TestGenerateSigningUrlPKCE:
    """Test generate_signing_url method with PKCE parameters."""

    def test_generate_signing_url_with_pkce_params(self):
        """Test URL generation with PKCE code_challenge parameters."""
        from pygeai_gam import generate_code_verifier, generate_code_challenge

        client = GAMClient(base_url="https://gam.test.com")
        verifier = generate_code_verifier()
        challenge = generate_code_challenge(verifier)

        url = client.generate_signing_url(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
            code_challenge=challenge,
            code_challenge_method="S256",
        )

        assert "code_challenge=" in url
        assert "code_challenge_method=S256" in url
        assert "client_id=test_client" in url
        assert "state=state123" in url

    def test_generate_signing_url_pkce_backward_compatible(self):
        """Test that generate_signing_url works without PKCE params."""
        client = GAMClient(base_url="https://gam.test.com")
        url = client.generate_signing_url(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
        )

        assert "code_challenge" not in url
        assert "client_id=test_client" in url

    def test_generate_signing_url_with_code_challenge_only(self):
        """Test URL generation with code_challenge but no method."""
        from pygeai_gam import generate_code_verifier, generate_code_challenge

        client = GAMClient(base_url="https://gam.test.com")
        verifier = generate_code_verifier()
        challenge = generate_code_challenge(verifier)

        url = client.generate_signing_url(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
            code_challenge=challenge,
        )

        assert "code_challenge=" in url
        assert "code_challenge_method" not in url

    def test_generate_signing_url_invalid_code_challenge_method(self):
        """Test that invalid code_challenge_method raises ValueError."""
        client = GAMClient(base_url="https://gam.test.com")

        with pytest.raises(ValueError) as exc_info:
            client.generate_signing_url(
                client_id="test_client",
                redirect_uri="https://app.com/callback",
                state="state123",
                code_challenge="some_challenge",
                code_challenge_method="plain",
            )

        assert "S256" in str(exc_info.value)
        assert "plain" in str(exc_info.value)


class TestInitiatePKCEFlow:
    """Test initiate_pkce_flow method."""

    def test_initiate_pkce_flow_success(self):
        """Test successful PKCE flow initiation."""
        client = GAMClient(base_url="https://gam.test.com")
        auth_url, code_verifier = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="random_state",
        )

        assert isinstance(auth_url, str)
        assert isinstance(code_verifier, str)
        assert len(code_verifier) == 43
        assert "https://gam.test.com/oauth/gam/signin?" in auth_url
        assert "code_challenge=" in auth_url
        assert "code_challenge_method=S256" in auth_url
        assert "client_id=test_client" in auth_url
        assert "state=random_state" in auth_url

    def test_initiate_pkce_flow_with_custom_scope(self):
        """Test PKCE flow initiation with custom scope."""
        client = GAMClient(base_url="https://gam.test.com")
        auth_url, code_verifier = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
            scope="gam_user_data+gam_user_roles",
        )

        assert "scope=gam_user_data%2Bgam_user_roles" in auth_url
        assert len(code_verifier) == 43

    def test_initiate_pkce_flow_generates_unique_verifiers(self):
        """Test that multiple calls generate different verifiers."""
        client = GAMClient(base_url="https://gam.test.com")

        _, verifier1 = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state1",
        )

        _, verifier2 = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state2",
        )

        assert verifier1 != verifier2

    def test_initiate_pkce_flow_code_verifier_valid(self):
        """Test that generated code_verifier is valid."""
        from pygeai_gam.pkce import _validate_code_verifier

        client = GAMClient(base_url="https://gam.test.com")
        _, code_verifier = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
        )

        _validate_code_verifier(code_verifier)


class TestExchangeCodeForToken:
    """Test exchange_code_for_token method."""

    @patch("pygeai_gam.client.requests.request")
    def test_exchange_code_for_token_success(self, mock_request):
        """Test successful authorization code exchange."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "access_token": "test_access_token",
            "token_type": "Bearer",
            "expires_in": 3600,
            "refresh_token": "test_refresh_token",
            "scope": "gam_user_data",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.exchange_code_for_token(
            code="auth_code_123",
            code_verifier="test_verifier_" + ("a" * 30),
            client_id="test_client",
            redirect_uri="https://app.com/callback",
        )

        assert response["access_token"] == "test_access_token"
        assert response["refresh_token"] == "test_refresh_token"
        assert response["expires_in"] == 3600

        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["data"]["grant_type"] == "authorization_code"
        assert call_kwargs["data"]["code"] == "auth_code_123"
        assert "test_verifier_" in call_kwargs["data"]["code_verifier"]
        assert call_kwargs["data"]["client_id"] == "test_client"
        assert call_kwargs["data"]["redirect_uri"] == "https://app.com/callback"

    @patch("pygeai_gam.client.requests.request")
    def test_exchange_code_for_token_with_client_secret(self, mock_request):
        """Test authorization code exchange with client_secret."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {"access_token": "token"}
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        response = client.exchange_code_for_token(
            code="auth_code",
            code_verifier="test_verifier_" + ("a" * 30),
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            client_secret="secret123",
        )

        assert response["access_token"] == "token"
        call_kwargs = mock_request.call_args[1]
        assert call_kwargs["data"]["client_secret"] == "secret123"

    @patch("pygeai_gam.client.requests.request")
    def test_exchange_code_for_token_invalid_code_raises_auth_error(self, mock_request):
        """Test that invalid authorization code raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.ok = False
        mock_response.text = "Invalid code"
        mock_response.json.return_value = {
            "error": "invalid_grant",
            "error_description": "Authorization code is invalid or expired",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError) as exc_info:
            client.exchange_code_for_token(
                code="invalid_code",
                code_verifier="test_verifier_" + ("a" * 30),
                client_id="test_client",
                redirect_uri="https://app.com/callback",
            )

        assert exc_info.value.status_code == 401
        assert "invalid" in str(exc_info.value).lower()

    @patch("pygeai_gam.client.requests.request")
    def test_exchange_code_for_token_verifier_mismatch_raises_error(self, mock_request):
        """Test that code_verifier mismatch raises GAMAuthenticationError."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.ok = False
        mock_response.text = "Code verifier mismatch"
        mock_response.json.return_value = {
            "error": "invalid_grant",
            "error_description": "Code verifier does not match code challenge",
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMAuthenticationError):
            client.exchange_code_for_token(
                code="auth_code",
                code_verifier="wrong_verifier_" + ("a" * 28),
                client_id="test_client",
                redirect_uri="https://app.com/callback",
            )

    def test_exchange_code_for_token_missing_code_raises_error(self):
        """Test that missing code raises GAMMissingParameterError."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError):
            client.exchange_code_for_token(
                code="",
                code_verifier="test_verifier_" + ("a" * 30),
                client_id="test_client",
                redirect_uri="https://app.com/callback",
            )

    def test_exchange_code_for_token_missing_verifier_raises_error(self):
        """Test that missing code_verifier raises GAMMissingParameterError."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError):
            client.exchange_code_for_token(
                code="auth_code",
                code_verifier="",
                client_id="test_client",
                redirect_uri="https://app.com/callback",
            )

    def test_exchange_code_for_token_missing_client_id_raises_error(self):
        """Test that missing client_id raises GAMMissingParameterError."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError):
            client.exchange_code_for_token(
                code="auth_code",
                code_verifier="test_verifier_" + ("a" * 30),
                client_id="",
                redirect_uri="https://app.com/callback",
            )

    def test_exchange_code_for_token_missing_redirect_uri_raises_error(self):
        """Test that missing redirect_uri raises GAMMissingParameterError."""
        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMMissingParameterError):
            client.exchange_code_for_token(
                code="auth_code",
                code_verifier="test_verifier_" + ("a" * 30),
                client_id="test_client",
                redirect_uri="",
            )

    @patch("pygeai_gam.client.requests.request")
    def test_exchange_code_for_token_invalid_json_raises_error(self, mock_request):
        """Test that invalid JSON response raises GAMInvalidResponseError."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.text = "Not JSON"
        mock_response.json.side_effect = ValueError("Invalid JSON")
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")
        with pytest.raises(GAMInvalidResponseError):
            client.exchange_code_for_token(
                code="auth_code",
                code_verifier="test_verifier_" + ("a" * 30),
                client_id="test_client",
                redirect_uri="https://app.com/callback",
            )


class TestPKCEFlowIntegration:
    """Integration tests for complete PKCE flow."""

    @patch("pygeai_gam.client.requests.request")
    def test_complete_pkce_flow_end_to_end(self, mock_request):
        """Test complete PKCE flow from initiation to token exchange."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.ok = True
        mock_response.json.return_value = {
            "access_token": "final_access_token",
            "refresh_token": "final_refresh_token",
            "expires_in": 3600,
        }
        mock_request.return_value = mock_response

        client = GAMClient(base_url="https://gam.test.com")

        auth_url, code_verifier = client.initiate_pkce_flow(
            client_id="my_app",
            redirect_uri="https://myapp.com/callback",
            state="csrf_token_123",
        )

        assert "code_challenge=" in auth_url
        assert "code_challenge_method=S256" in auth_url
        assert len(code_verifier) == 43

        tokens = client.exchange_code_for_token(
            code="authorization_code_from_callback",
            code_verifier=code_verifier,
            client_id="my_app",
            redirect_uri="https://myapp.com/callback",
        )

        assert tokens["access_token"] == "final_access_token"
        assert tokens["refresh_token"] == "final_refresh_token"

    def test_pkce_flow_code_challenge_in_url_matches_verifier(self):
        """Test that code_challenge in URL can be verified with code_verifier."""
        from pygeai_gam import generate_code_challenge
        from urllib.parse import parse_qs, urlparse

        client = GAMClient(base_url="https://gam.test.com")
        auth_url, code_verifier = client.initiate_pkce_flow(
            client_id="test_client",
            redirect_uri="https://app.com/callback",
            state="state123",
        )

        parsed_url = urlparse(auth_url)
        query_params = parse_qs(parsed_url.query)
        code_challenge_from_url = query_params["code_challenge"][0]

        expected_challenge = generate_code_challenge(code_verifier)
        assert code_challenge_from_url == expected_challenge
