"""
Tests for OAuth utilities (redirect URI resolution).
"""

import pytest
import socket
from unittest.mock import patch, MagicMock
from pygeai_gam.utils import (
    find_available_port,
    extract_port_from_uri,
    resolve_redirect_uri,
    DYNAMIC_PORT_PLACEHOLDER
)


class TestFindAvailablePort:
    """Tests for find_available_port() function."""
    
    def test_returns_valid_port(self):
        """Test that find_available_port returns a valid port number."""
        port = find_available_port()
        
        assert isinstance(port, int)
        assert port > 0
        assert port <= 65535
    
    def test_returns_different_ports(self):
        """Test that consecutive calls return different ports (usually)."""
        port1 = find_available_port()
        port2 = find_available_port()
        
        # Ports should be valid
        assert isinstance(port1, int) and port1 > 0
        assert isinstance(port2, int) and port2 > 0
    
    def test_port_is_actually_available(self):
        """Test that returned port can be bound."""
        port = find_available_port()
        
        # Try to bind to the port - should succeed
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.bind(("localhost", port))
            _, bound_port = sock.getsockname()
            assert bound_port == port
    
    @patch('socket.socket')
    def test_raises_oserror_on_failure(self, mock_socket):
        """Test that OSError is raised when port finding fails."""
        mock_sock = MagicMock()
        mock_sock.bind.side_effect = OSError("Cannot bind")
        mock_sock.__enter__.return_value = mock_sock
        mock_sock.__exit__.return_value = None
        mock_socket.return_value = mock_sock
        
        with pytest.raises(OSError):
            find_available_port()


class TestExtractPortFromUri:
    """Tests for extract_port_from_uri() function."""
    
    def test_extract_port_from_valid_uri(self):
        """Test extracting port from valid URI."""
        uri = "http://localhost:8080/callback"
        port = extract_port_from_uri(uri)
        assert port == 8080
    
    def test_extract_port_from_https_uri(self):
        """Test extracting port from HTTPS URI."""
        uri = "https://localhost:9000/auth/callback"
        port = extract_port_from_uri(uri)
        assert port == 9000
    
    def test_extract_port_with_different_host(self):
        """Test extracting port with different hostname."""
        uri = "http://127.0.0.1:3000/callback"
        port = extract_port_from_uri(uri)
        assert port == 3000
    
    def test_extract_high_port_number(self):
        """Test extracting high port number."""
        uri = "http://localhost:65535/callback"
        port = extract_port_from_uri(uri)
        assert port == 65535
    
    def test_raises_valueerror_without_port(self):
        """Test that ValueError is raised when URI has no port."""
        uri = "http://localhost/callback"
        
        with pytest.raises(ValueError) as exc_info:
            extract_port_from_uri(uri)
        
        assert "No port found" in str(exc_info.value)
    
    def test_raises_valueerror_with_invalid_uri(self):
        """Test that ValueError is raised for invalid URI."""
        uri = "not-a-valid-uri"
        
        with pytest.raises(ValueError):
            extract_port_from_uri(uri)


class TestResolveRedirectUri:
    """Tests for resolve_redirect_uri() function."""
    
    def test_resolve_with_xxxx_placeholder(self):
        """Test resolving URI with XXXX placeholder."""
        uri_template = "http://localhost:XXXX/callback"
        
        resolved_uri, port = resolve_redirect_uri(uri_template)
        
        # Port should be valid
        assert isinstance(port, int)
        assert port > 0
        assert port <= 65535
        
        # URI should have XXXX replaced
        assert "XXXX" not in resolved_uri
        assert f"http://localhost:{port}/callback" == resolved_uri
    
    def test_resolve_with_xxxx_in_different_position(self):
        """Test resolving URI with XXXX in different context."""
        uri_template = "http://127.0.0.1:XXXX/auth/redirect"
        
        resolved_uri, port = resolve_redirect_uri(uri_template)
        
        assert "XXXX" not in resolved_uri
        assert resolved_uri.startswith("http://127.0.0.1:")
        assert resolved_uri.endswith("/auth/redirect")
        assert isinstance(port, int)
    
    def test_resolve_with_fixed_port(self):
        """Test resolving URI with fixed port (no XXXX)."""
        uri = "http://localhost:8080/callback"
        
        resolved_uri, port = resolve_redirect_uri(uri)
        
        # Should return unchanged
        assert resolved_uri == uri
        assert port == 8080
    
    def test_resolve_with_different_fixed_port(self):
        """Test resolving with different fixed port."""
        uri = "http://localhost:3000/auth"
        
        resolved_uri, port = resolve_redirect_uri(uri)
        
        assert resolved_uri == uri
        assert port == 3000
    
    def test_resolve_with_https(self):
        """Test resolving HTTPS URI with XXXX."""
        uri_template = "https://localhost:XXXX/secure/callback"
        
        resolved_uri, port = resolve_redirect_uri(uri_template)
        
        assert "XXXX" not in resolved_uri
        assert resolved_uri.startswith("https://localhost:")
        assert "/secure/callback" in resolved_uri
    
    def test_raises_valueerror_for_invalid_uri(self):
        """Test that ValueError is raised for invalid URI without port."""
        uri = "http://localhost/callback"
        
        with pytest.raises(ValueError) as exc_info:
            resolve_redirect_uri(uri)
        
        assert "No port found" in str(exc_info.value)
    
    def test_multiple_calls_return_different_ports(self):
        """Test that multiple XXXX resolutions return different ports."""
        uri_template = "http://localhost:XXXX/callback"
        
        _, port1 = resolve_redirect_uri(uri_template)
        _, port2 = resolve_redirect_uri(uri_template)
        
        # Both should be valid ports
        assert isinstance(port1, int) and port1 > 0
        assert isinstance(port2, int) and port2 > 0
        # They might be different (not guaranteed but very likely)
    
    @patch('pygeai_gam.utils.find_available_port')
    def test_uses_find_available_port_for_xxxx(self, mock_find_port):
        """Test that find_available_port is called for XXXX resolution."""
        mock_find_port.return_value = 12345
        uri_template = "http://localhost:XXXX/callback"
        
        resolved_uri, port = resolve_redirect_uri(uri_template)
        
        mock_find_port.assert_called_once()
        assert port == 12345
        assert "12345" in resolved_uri
    
    @patch('pygeai_gam.utils.find_available_port')
    def test_oserror_propagated_from_port_finding(self, mock_find_port):
        """Test that OSError from find_available_port is propagated."""
        mock_find_port.side_effect = OSError("Cannot find port")
        uri_template = "http://localhost:XXXX/callback"
        
        with pytest.raises(OSError):
            resolve_redirect_uri(uri_template)


class TestDynamicPortPlaceholder:
    """Tests for DYNAMIC_PORT_PLACEHOLDER constant."""
    
    def test_placeholder_is_xxxx(self):
        """Test that the placeholder constant is 'XXXX'."""
        assert DYNAMIC_PORT_PLACEHOLDER == "XXXX"
    
    def test_placeholder_is_string(self):
        """Test that the placeholder is a string."""
        assert isinstance(DYNAMIC_PORT_PLACEHOLDER, str)
