"""
Tests for GAMClient browser-based authentication flows.
"""

import pytest
from unittest.mock import Mock, patch

from pygeai_gam import (
    GAMClient,
    GAMAuthenticationError,
)


class TestGAMClientGetAuthorizationCodeViaBrowser:
    """Tests for get_authorization_code_via_browser method."""
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_success_flow(self, mock_server_class, mock_webbrowser):
        """Test successful authorization code retrieval via browser."""
        client = GAMClient(base_url="https://gam.example.com")
        
        # Mock the server
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        # Execute
        code, verifier = client.get_authorization_code_via_browser(
            client_id="test_client",
            redirect_uri="http://localhost:8080/callback",
            state="test_state",
            callback_port=8080
        )
        
        # Verify server was created with correct params
        mock_server_class.assert_called_once_with(
            port=8080,
            timeout=300,
            success_html=None,
            error_html=None,
            host='localhost'
        )
        
        # Verify server lifecycle
        mock_server.start.assert_called_once()
        mock_server.stop.assert_called_once()
        
        # Verify browser was opened
        assert mock_webbrowser.open.called
        
        # Verify callback was waited for
        mock_server.wait_for_callback.assert_called_once_with(expected_state='test_state')
        
        # Verify return values
        assert code == 'test_auth_code'
        assert verifier is not None
        assert len(verifier) > 0
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_custom_html(self, mock_server_class, mock_webbrowser):
        """Test with custom HTML responses."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        custom_success = "<html>Custom Success</html>"
        custom_error = "<html>Custom Error</html>"
        
        client.get_authorization_code_via_browser(
            client_id="test_client",
            redirect_uri="http://localhost:8080/callback",
            state="test_state",
            callback_success_html=custom_success,
            callback_error_html=custom_error
        )
        
        # Verify custom HTML was passed
        mock_server_class.assert_called_once()
        call_kwargs = mock_server_class.call_args[1]
        assert call_kwargs['success_html'] == custom_success
        assert call_kwargs['error_html'] == custom_error
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_custom_browser_opener(self, mock_server_class, mock_webbrowser):
        """Test with custom browser opener function."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        custom_opener = Mock()
        
        client.get_authorization_code_via_browser(
            client_id="test_client",
            redirect_uri="http://localhost:8080/callback",
            state="test_state",
            browser_opener=custom_opener
        )
        
        # Verify custom opener was called instead of webbrowser
        assert custom_opener.called
        assert not mock_webbrowser.open.called
        
        # Verify opener was called with the auth URL
        auth_url = custom_opener.call_args[0][0]
        assert 'gam.example.com' in auth_url
        assert 'test_client' in auth_url
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_no_browser_opening(self, mock_server_class, mock_webbrowser):
        """Test with browser opening disabled."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        client.get_authorization_code_via_browser(
            client_id="test_client",
            redirect_uri="http://localhost:8080/callback",
            state="test_state",
            open_browser=False
        )
        
        # Verify browser was NOT opened
        assert not mock_webbrowser.open.called
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_custom_port_and_timeout(self, mock_server_class, mock_webbrowser):
        """Test with custom port and timeout."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        client.get_authorization_code_via_browser(
            client_id="test_client",
            redirect_uri="http://localhost:9000/callback",
            state="test_state",
            callback_port=9000,
            callback_timeout=600
        )
        
        # Verify custom port and timeout
        call_kwargs = mock_server_class.call_args[1]
        assert call_kwargs['port'] == 9000
        assert call_kwargs['timeout'] == 600
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_server_stopped_on_error(self, mock_server_class, mock_webbrowser):
        """Test that server is stopped even when error occurs."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.side_effect = GAMAuthenticationError("Test error", status_code=400)
        
        with pytest.raises(GAMAuthenticationError):
            client.get_authorization_code_via_browser(
                client_id="test_client",
                redirect_uri="http://localhost:8080/callback",
                state="test_state"
            )
        
        # Verify server was stopped despite error
        mock_server.stop.assert_called_once()


class TestGAMClientCompletePKCEFlowViaBrowser:
    """Tests for complete_pkce_flow_via_browser method."""
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_success_flow(self, mock_server_class, mock_webbrowser):
        """Test successful complete PKCE flow."""
        client = GAMClient(base_url="https://gam.example.com")
        
        # Mock the server
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        # Mock the token exchange
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.return_value = {
                'access_token': 'test_access_token',
                'refresh_token': 'test_refresh_token',
                'expires_in': 3600
            }
            
            # Mock user info fetch
            with patch.object(client, 'get_user_info') as mock_user_info:
                mock_user_info.return_value = {
                    'username': 'testuser',
                    'email': 'test@example.com'
                }
                
                # Execute
                tokens = client.complete_pkce_flow_via_browser(
                    client_id="test_client",
                    redirect_uri="http://localhost:8080/callback",
                    state="test_state",
                    callback_port=8080,
                    fetch_user_info=True
                )
        
        # Verify token exchange was called
        assert mock_exchange.called
        exchange_args = mock_exchange.call_args[1]
        assert exchange_args['code'] == 'test_auth_code'
        assert exchange_args['client_id'] == 'test_client'
        assert 'code_verifier' in exchange_args
        
        # Verify user info was fetched
        mock_user_info.assert_called_once_with(access_token='test_access_token')
        
        # Verify return value includes user info
        assert tokens['access_token'] == 'test_access_token'
        assert tokens['user_info']['username'] == 'testuser'
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_without_user_info(self, mock_server_class, mock_webbrowser):
        """Test flow without fetching user info."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.return_value = {
                'access_token': 'test_access_token',
                'refresh_token': 'test_refresh_token',
                'expires_in': 3600
            }
            
            with patch.object(client, 'get_user_info') as mock_user_info:
                # Execute without fetching user info
                tokens = client.complete_pkce_flow_via_browser(
                    client_id="test_client",
                    redirect_uri="http://localhost:8080/callback",
                    state="test_state",
                    fetch_user_info=False
                )
        
        # Verify user info was NOT fetched
        assert not mock_user_info.called
        assert 'user_info' not in tokens
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_with_client_secret(self, mock_server_class, mock_webbrowser):
        """Test flow with client secret for confidential clients."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.return_value = {
                'access_token': 'test_access_token',
                'expires_in': 3600
            }
            
            # Execute with client secret
            client.complete_pkce_flow_via_browser(
                client_id="test_client",
                redirect_uri="http://localhost:8080/callback",
                state="test_state",
                client_secret="test_secret",
                fetch_user_info=False
            )
        
        # Verify client secret was passed to exchange
        exchange_args = mock_exchange.call_args[1]
        assert exchange_args['client_secret'] == 'test_secret'
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_all_custom_parameters(self, mock_server_class, mock_webbrowser):
        """Test flow with all customization options."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        custom_opener = Mock()
        
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.return_value = {
                'access_token': 'test_access_token',
                'expires_in': 3600
            }
            
            # Execute with all custom params
            _ = client.complete_pkce_flow_via_browser(
                client_id="test_client",
                redirect_uri="http://localhost:9000/callback",
                state="test_state",
                scope="custom_scope",
                callback_port=9000,
                callback_timeout=600,
                callback_success_html="<html>Success</html>",
                callback_error_html="<html>Error</html>",
                open_browser=False,
                browser_opener=custom_opener,
                client_secret="secret",
                fetch_user_info=False
            )
        
        # Verify custom parameters were used
        assert custom_opener.called
        call_kwargs = mock_server_class.call_args[1]
        assert call_kwargs['port'] == 9000
        assert call_kwargs['timeout'] == 600
        assert call_kwargs['success_html'] == "<html>Success</html>"
        assert call_kwargs['error_html'] == "<html>Error</html>"
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_token_exchange_error(self, mock_server_class, mock_webbrowser):
        """Test handling of token exchange error."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.side_effect = GAMAuthenticationError("Invalid code", status_code=400)
            
            with pytest.raises(GAMAuthenticationError):
                client.complete_pkce_flow_via_browser(
                    client_id="test_client",
                    redirect_uri="http://localhost:8080/callback",
                    state="test_state"
                )
        
        # Verify server was still stopped
        mock_server.stop.assert_called_once()
    
    @patch('pygeai_gam.client.webbrowser')
    @patch('pygeai_gam.client.OAuthCallbackServer')
    def test_user_info_error_propagates(self, mock_server_class, mock_webbrowser):
        """Test that user info fetch errors propagate."""
        client = GAMClient(base_url="https://gam.example.com")
        
        mock_server = Mock()
        mock_server_class.return_value = mock_server
        mock_server.wait_for_callback.return_value = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        with patch.object(client, 'exchange_code_for_token') as mock_exchange:
            mock_exchange.return_value = {
                'access_token': 'test_access_token',
                'expires_in': 3600
            }
            
            with patch.object(client, 'get_user_info') as mock_user_info:
                mock_user_info.side_effect = GAMAuthenticationError("Token expired", status_code=401)
                
                with pytest.raises(GAMAuthenticationError):
                    client.complete_pkce_flow_via_browser(
                        client_id="test_client",
                        redirect_uri="http://localhost:8080/callback",
                        state="test_state",
                        fetch_user_info=True
                    )
