"""
Tests for OAuth callback server functionality.
"""

import pytest
from unittest.mock import Mock, patch
from threading import Event
from pygeai_gam.oauth_callback import OAuthCallbackServer, OAuthCallbackHandler
from pygeai_gam.exceptions import GAMAuthenticationError, GAMAPIError


class TestOAuthCallbackServer:
    """Tests for OAuthCallbackServer class."""
    
    def test_init_default_values(self):
        """Test initialization with default values."""
        server = OAuthCallbackServer()
        assert server.port == 8080
        assert server.timeout == 300
        assert server.host == 'localhost'
        assert 'Authentication Successful' in server.success_html
        assert 'Authentication Failed' in server.error_html
    
    def test_init_custom_values(self):
        """Test initialization with custom values."""
        custom_success = "<html>Custom Success</html>"
        custom_error = "<html>Custom Error: {error}</html>"
        
        server = OAuthCallbackServer(
            port=9090,
            timeout=600,
            success_html=custom_success,
            error_html=custom_error,
            host='127.0.0.1'
        )
        
        assert server.port == 9090
        assert server.timeout == 600
        assert server.host == '127.0.0.1'
        assert server.success_html == custom_success
        assert server.error_html == custom_error
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_start_server(self, mock_thread, mock_http_server):
        """Test starting the callback server."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        mock_http_server.assert_called_once_with(('localhost', 8080), OAuthCallbackHandler)
        mock_thread.assert_called_once()
        assert mock_thread.call_args[1]['daemon'] is True
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    def test_stop_server(self, mock_http_server):
        """Test stopping the callback server."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        server.stop()
        
        mock_server_instance.shutdown.assert_called_once()
        assert server._server is None
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_wait_for_callback_success(self, mock_thread, mock_http_server):
        """Test successful callback reception."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Simulate successful callback
        server._callback_received.set()
        mock_server_instance.callback_params = {
            'code': 'test_auth_code',
            'state': 'test_state'
        }
        
        result = server.wait_for_callback(expected_state='test_state')
        
        assert result['code'] == 'test_auth_code'
        assert result['state'] == 'test_state'
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_wait_for_callback_state_mismatch(self, mock_thread, mock_http_server):
        """Test callback with mismatched state (CSRF protection)."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Simulate callback with wrong state
        server._callback_received.set()
        mock_server_instance.callback_params = {
            'code': 'test_auth_code',
            'state': 'wrong_state'
        }
        
        with pytest.raises(GAMAuthenticationError, match="State mismatch"):
            server.wait_for_callback(expected_state='correct_state')
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_wait_for_callback_oauth_error(self, mock_thread, mock_http_server):
        """Test callback with OAuth error."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Simulate OAuth error callback
        server._callback_received.set()
        mock_server_instance.callback_params = {
            'error': 'access_denied',
            'error_description': 'User denied access',
            'state': 'test_state'
        }
        
        with pytest.raises(GAMAuthenticationError, match="User denied access"):
            server.wait_for_callback(expected_state='test_state')
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_wait_for_callback_missing_code(self, mock_thread, mock_http_server):
        """Test callback without authorization code."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Simulate callback without code
        server._callback_received.set()
        mock_server_instance.callback_params = {
            'state': 'test_state'
        }
        
        with pytest.raises(GAMAuthenticationError, match="No authorization code"):
            server.wait_for_callback(expected_state='test_state')
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_wait_for_callback_timeout(self, mock_thread, mock_http_server):
        """Test callback timeout."""
        server = OAuthCallbackServer(port=8080, timeout=1)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Don't trigger the callback event
        with pytest.raises(GAMAPIError, match="timeout"):
            server.wait_for_callback(expected_state='test_state')
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_context_manager(self, mock_thread, mock_http_server):
        """Test using server as context manager."""
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        with OAuthCallbackServer(port=8080):
            # Server should be started
            mock_http_server.assert_called_once()
        
        # Server should be stopped after context
        mock_server_instance.shutdown.assert_called_once()
    
    def test_wait_before_start_raises_error(self):
        """Test that waiting without starting raises error."""
        server = OAuthCallbackServer(port=8080)
        
        with pytest.raises(GAMAPIError, match="Server not started"):
            server.wait_for_callback(expected_state='test_state')
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    def test_start_server_error(self, mock_http_server):
        """Test error handling when server fails to start."""
        mock_http_server.side_effect = OSError("Address already in use")
        
        server = OAuthCallbackServer(port=8080)
        
        with pytest.raises(GAMAPIError, match="Failed to start OAuth callback server"):
            server.start()
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_stop_without_server(self, mock_thread, mock_http_server):
        """Test that stopping without starting doesn't raise error."""
        server = OAuthCallbackServer(port=8080)
        
        # Should not raise an error
        server.stop()
    
    @patch('pygeai_gam.oauth_callback.HTTPServer')
    @patch('pygeai_gam.oauth_callback.Thread')
    def test_oauth_error_without_description(self, mock_thread, mock_http_server):
        """Test callback with OAuth error but no description."""
        server = OAuthCallbackServer(port=8080)
        mock_server_instance = Mock()
        mock_http_server.return_value = mock_server_instance
        
        server.start()
        
        # Simulate OAuth error without description
        server._callback_received.set()
        mock_server_instance.callback_params = {
            'error': 'invalid_request',
            'state': 'test_state'
        }
        
        with pytest.raises(GAMAuthenticationError, match="invalid_request"):
            server.wait_for_callback(expected_state='test_state')


class TestOAuthCallbackHandler:
    """Tests for OAuthCallbackHandler class."""
    
    def test_handler_success_response(self):
        """Test handler processes successful OAuth callback."""
        from io import BytesIO
        
        # Create handler instance without calling __init__
        handler = OAuthCallbackHandler.__new__(OAuthCallbackHandler)
        
        # Manually set required attributes
        handler.server = Mock()
        handler.server.callback_received = Event()
        handler.server.callback_params = {}
        handler.server.success_html = "<html>Success</html>"
        handler.server.error_html = "<html>Error: {error}</html>"
        
        handler.path = '/callback?code=auth_code_123&state=test_state'
        handler.wfile = BytesIO()
        handler.send_response = Mock()
        handler.send_header = Mock()
        handler.end_headers = Mock()
        
        # Execute the handler
        handler.do_GET()
        
        # Verify callback was processed
        assert handler.server.callback_received.is_set()
        assert handler.server.callback_params['code'] == 'auth_code_123'
        assert handler.server.callback_params['state'] == 'test_state'
        
        # Verify response was sent
        handler.send_response.assert_called_once_with(200)
        assert handler.wfile.getvalue() == b'<html>Success</html>'
    
    def test_handler_error_response(self):
        """Test handler processes OAuth error callback."""
        from io import BytesIO
        
        handler = OAuthCallbackHandler.__new__(OAuthCallbackHandler)
        
        handler.server = Mock()
        handler.server.callback_received = Event()
        handler.server.callback_params = {}
        handler.server.success_html = "<html>Success</html>"
        handler.server.error_html = "<html>Error: {error} - {error_description}</html>"
        
        handler.path = '/callback?error=access_denied&error_description=User%20denied&state=test_state'
        handler.wfile = BytesIO()
        handler.send_response = Mock()
        handler.send_header = Mock()
        handler.end_headers = Mock()
        
        handler.do_GET()
        
        assert handler.server.callback_received.is_set()
        assert handler.server.callback_params['error'] == 'access_denied'
        assert 'User denied' in handler.server.callback_params['error_description']
        
        # Verify error HTML was formatted
        response = handler.wfile.getvalue().decode('utf-8')
        assert 'access_denied' in response
    
    def test_handler_no_query_string(self):
        """Test handler when path has no query string."""
        from io import BytesIO
        
        handler = OAuthCallbackHandler.__new__(OAuthCallbackHandler)
        
        handler.server = Mock()
        handler.server.callback_received = Event()
        handler.server.callback_params = {}
        handler.path = '/callback'
        handler.wfile = BytesIO()
        handler.send_response = Mock()
        handler.send_header = Mock()
        handler.end_headers = Mock()
        
        # This should not set callback_received or write anything
        handler.do_GET()
        
        # Callback should not be triggered without query parameters
        assert not handler.server.callback_received.is_set()
    
    def test_handler_log_message_suppression(self):
        """Test that log_message is suppressed."""
        handler = OAuthCallbackHandler.__new__(OAuthCallbackHandler)
        
        # This should not raise or log anything
        result = handler.log_message("Test message %s", "arg")
        assert result is None
    
    def test_handler_server_version(self):
        """Test that server version is set correctly."""
        # Check class attribute
        assert OAuthCallbackHandler.server_version == "pygeai-gam/1.0"
