"""
Tests for security features and middleware.
"""

import base64
from unittest.mock import AsyncMock, MagicMock

import pytest

from velithon.exceptions import HTTPException
from velithon.middleware.session import SignedCookieSessionInterface
from velithon.responses import JSONResponse


class TestSecurityHeaders:
    """Test security-related headers and middleware."""

    def test_security_headers_response(self):
        """Test that security headers can be added to responses."""
        security_headers = {
            'X-Content-Type-Options': 'nosniff',
            'X-Frame-Options': 'DENY',
            'X-XSS-Protection': '1; mode=block',
            'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
            'Content-Security-Policy': "default-src 'self'",
        }

        response = JSONResponse(content={'message': 'secure'}, headers=security_headers)

        response_headers = dict(response.raw_headers)
        for header, value in security_headers.items():
            assert response_headers.get(header.lower()) == value

    def test_server_header_hiding(self):
        """Test that server headers can be customized or hidden."""
        response = JSONResponse(
            content={'message': 'test'}, headers={'Server': 'Custom/1.0'}
        )

        response_headers = dict(response.raw_headers)
        assert response_headers.get('server') == 'Custom/1.0'


class TestSignedCookieSecurity:
    """Test signed cookie security features."""

    @pytest.fixture
    def cookie_interface(self):
        """Create a signed cookie interface for testing."""
        return SignedCookieSessionInterface('test-secret-key-123', max_age=3600)

    def test_data_signing_integrity(self, cookie_interface):
        """Test that signed data maintains integrity."""
        original_data = 'sensitive_user_data'
        signed_data = cookie_interface._sign_data(original_data)

        # Should be different from original
        assert signed_data != original_data

        # Should contain signature separator
        assert '.' in signed_data

        # Should verify correctly
        unsigned_data = cookie_interface._unsign_data(signed_data)
        assert unsigned_data == original_data

    def test_signature_tampering_detection(self, cookie_interface):
        """Test that signature tampering is detected."""
        original_data = 'user_id=123'
        signed_data = cookie_interface._sign_data(original_data)

        # Tamper with the data part
        data_part, signature = signed_data.split('.', 1)
        tampered_data = base64.b64encode(b'user_id=999').decode()
        tampered_signed = f'{tampered_data}.{signature}'

        # Should fail verification
        result = cookie_interface._unsign_data(tampered_signed)
        assert result is None

    def test_signature_modification_detection(self, cookie_interface):
        """Test that signature modification is detected."""
        original_data = 'user_id=123'
        signed_data = cookie_interface._sign_data(original_data)

        # Modify the signature
        data_part, signature = signed_data.split('.', 1)
        modified_signature = signature[:-5] + 'xxxxx'  # Change last 5 chars
        modified_signed = f'{data_part}.{modified_signature}'

        # Should fail verification
        result = cookie_interface._unsign_data(modified_signed)
        assert result is None

    def test_empty_data_signing(self, cookie_interface):
        """Test signing of empty data."""
        signed_empty = cookie_interface._sign_data('')
        unsigned_empty = cookie_interface._unsign_data(signed_empty)

        assert unsigned_empty == ''

    def test_unicode_data_signing(self, cookie_interface):
        """Test signing of Unicode data."""
        unicode_data = '用户ID=123'
        signed_data = cookie_interface._sign_data(unicode_data)
        unsigned_data = cookie_interface._unsign_data(signed_data)

        assert unsigned_data == unicode_data

    def test_large_data_signing(self, cookie_interface):
        """Test signing of large data."""
        large_data = 'x' * 1000  # 1KB of data
        signed_data = cookie_interface._sign_data(large_data)
        unsigned_data = cookie_interface._unsign_data(signed_data)

        assert unsigned_data == large_data

    def test_malformed_signed_data(self, cookie_interface):
        """Test handling of malformed signed data."""
        malformed_cases = [
            'no_separator',
            '',
            'only.one.dot',
            '.empty_data',
            'empty_signature.',
            'invalid_base64!.valid_sig',
        ]

        for malformed in malformed_cases:
            result = cookie_interface._unsign_data(malformed)
            assert result is None

    def test_different_secret_keys(self):
        """Test that different secret keys produce different signatures."""
        data = 'test_data'

        interface1 = SignedCookieSessionInterface('secret1', max_age=3600)
        interface2 = SignedCookieSessionInterface('secret2', max_age=3600)

        signed1 = interface1._sign_data(data)
        signed2 = interface2._sign_data(data)

        # Should produce different signatures
        assert signed1 != signed2

        # Should not cross-verify
        assert interface1._unsign_data(signed2) is None
        assert interface2._unsign_data(signed1) is None


class TestSessionEncryption:
    """Test session data encryption and security."""

    @pytest.fixture
    def cookie_interface(self):
        """Create a signed cookie interface for testing."""
        return SignedCookieSessionInterface('secure-secret-key-256bit', max_age=3600)

    def test_session_data_encoding(self, cookie_interface):
        """Test session data encoding and decoding."""
        session_data = {
            'user_id': 123,
            'username': 'alice',
            'roles': ['user', 'admin'],
            'preferences': {'theme': 'dark', 'language': 'en'},
        }

        encoded = cookie_interface._encode_session(session_data)
        decoded = cookie_interface._decode_session(encoded)

        assert decoded == session_data

    def test_session_expiration_handling(self, cookie_interface):
        """Test session expiration handling."""
        import time

        # Create interface with very short expiration
        short_interface = SignedCookieSessionInterface('test-key', max_age=1)

        session_data = {'user_id': 123}
        encoded = short_interface._encode_session(session_data)

        # Wait for expiration
        time.sleep(2)

        # Should return empty dict for expired session
        decoded = short_interface._decode_session(encoded)
        assert decoded == {}

    def test_session_data_with_special_characters(self, cookie_interface):
        """Test session data with special characters."""
        session_data = {
            'message': 'Hello, 世界! 🌍',
            'special_chars': '!@#$%^&*()_+-=[]{}|;:\'",.<>?',
            'unicode': 'ñoño',
        }

        encoded = cookie_interface._encode_session(session_data)
        decoded = cookie_interface._decode_session(encoded)

        assert decoded == session_data

    def test_large_session_data(self, cookie_interface):
        """Test handling of large session data."""
        # Create session data close to cookie size limit
        large_data = {
            'large_field': 'x' * 3000,  # 3KB field
            'user_id': 123,
            'timestamp': 1640995200,
        }

        encoded = cookie_interface._encode_session(large_data)
        decoded = cookie_interface._decode_session(encoded)

        assert decoded == large_data

    def test_empty_session_data(self, cookie_interface):
        """Test handling of empty session data."""
        empty_data = {}

        encoded = cookie_interface._encode_session(empty_data)
        decoded = cookie_interface._decode_session(encoded)

        assert decoded == empty_data

    def test_none_session_data(self, cookie_interface):
        """Test handling of None session data."""
        # The method expects a string, so passing None should be handled gracefully
        try:
            decoded = cookie_interface._decode_session(None)
            assert decoded == {}
        except AttributeError:
            # This is expected since None doesn't have rsplit method
            # In practice, this would be handled at a higher level
            pass

    def test_invalid_session_data_format(self, cookie_interface):
        """Test handling of invalid session data format."""
        invalid_cases = [
            'invalid_json',
            '',
            'not.proper.format',
            base64.b64encode(b'invalid json').decode(),
        ]

        for invalid in invalid_cases:
            decoded = cookie_interface._decode_session(invalid)
            assert decoded == {}


class TestInputValidation:
    """Test input validation and sanitization."""

    def test_path_parameter_validation(self):
        """Test path parameter validation."""
        from velithon.responses import JSONResponse
        from velithon.routing import Route

        async def handler(user_id: int):
            if user_id < 0:
                raise HTTPException(status_code=400, detail='Invalid user ID')
            return JSONResponse({'user_id': user_id})

        route = Route('/users/{user_id}', handler)

        # Route should be created successfully
        assert route.path == '/users/{user_id}'

    def test_query_parameter_validation(self):
        """Test query parameter validation."""
        from pydantic import BaseModel, field_validator

        class QueryParams(BaseModel):
            page: int = 1
            limit: int = 10

            @field_validator('page')
            @classmethod
            def validate_page(cls, v):
                if v < 1:
                    raise ValueError('Page must be >= 1')
                return v

            @field_validator('limit')
            @classmethod
            def validate_limit(cls, v):
                if v < 1 or v > 100:
                    raise ValueError('Limit must be between 1 and 100')
                return v

        # Test valid params
        valid_params = QueryParams(page=1, limit=20)
        assert valid_params.page == 1
        assert valid_params.limit == 20

        # Test invalid params
        with pytest.raises(ValueError):
            QueryParams(page=0)

        with pytest.raises(ValueError):
            QueryParams(limit=200)

    def test_request_body_validation(self):
        """Test request body validation."""
        import re

        from pydantic import BaseModel, field_validator

        class UserCreate(BaseModel):
            username: str
            email: str
            password: str

            @field_validator('username')
            @classmethod
            def validate_username(cls, v):
                if len(v) < 3 or len(v) > 50:
                    raise ValueError('Username must be 3-50 characters')
                if not re.match(r'^[a-zA-Z0-9_]+$', v):
                    raise ValueError(
                        'Username can only contain letters, numbers, and underscores'
                    )
                return v

            @field_validator('email')
            @classmethod
            def validate_email(cls, v):
                if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
                    raise ValueError('Invalid email format')
                return v

            @field_validator('password')
            @classmethod
            def validate_password(cls, v):
                if len(v) < 8:
                    raise ValueError('Password must be at least 8 characters')
                if not re.search(r'[A-Z]', v):
                    raise ValueError('Password must contain uppercase letter')
                if not re.search(r'[a-z]', v):
                    raise ValueError('Password must contain lowercase letter')
                if not re.search(r'[0-9]', v):
                    raise ValueError('Password must contain digit')
                return v

        # Test valid user
        valid_user = UserCreate(
            username='alice123', email='alice@example.com', password='SecurePass123'
        )
        assert valid_user.username == 'alice123'

        # Test invalid cases
        with pytest.raises(ValueError):
            UserCreate(
                username='ab', email='valid@example.com', password='SecurePass123'
            )

        with pytest.raises(ValueError):
            UserCreate(
                username='alice123', email='invalid-email', password='SecurePass123'
            )

        with pytest.raises(ValueError):
            UserCreate(username='alice123', email='alice@example.com', password='weak')


class TestAuthenticationSecurity:
    """Test authentication and authorization security."""

    def test_bearer_token_validation(self):
        """Test Bearer token validation."""

        def validate_bearer_token(auth_header: str):
            if not auth_header:
                raise HTTPException(
                    status_code=401,
                    error='UNAUTHORIZED',
                    details={'message': 'Missing authorization header'},
                )

            parts = auth_header.split()
            if len(parts) != 2 or parts[0].lower() != 'bearer':
                raise HTTPException(
                    status_code=401,
                    error='UNAUTHORIZED',
                    details={'message': 'Invalid authorization header format'},
                )

            token = parts[1]
            if len(token) < 10:  # Simple validation
                raise HTTPException(
                    status_code=401,
                    error='UNAUTHORIZED',
                    details={'message': 'Invalid token'},
                )

            return token

        # Test valid token
        valid_header = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
        token = validate_bearer_token(valid_header)
        assert token == 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'

        # Test invalid cases
        with pytest.raises(HTTPException):
            validate_bearer_token('')

        with pytest.raises(HTTPException):
            validate_bearer_token('Invalid format')

        with pytest.raises(HTTPException):
            validate_bearer_token('Bearer short')

    def test_api_key_validation(self):
        """Test API key validation."""
        VALID_API_KEYS = {
            'api_key_123': {'user': 'alice', 'permissions': ['read', 'write']},
            'api_key_456': {'user': 'bob', 'permissions': ['read']},
        }

        def validate_api_key(api_key: str):
            if not api_key:
                raise HTTPException(
                    status_code=401,
                    error='UNAUTHORIZED',
                    details={'message': 'Missing API key'},
                )

            if api_key not in VALID_API_KEYS:
                raise HTTPException(
                    status_code=401,
                    error='UNAUTHORIZED',
                    details={'message': 'Invalid API key'},
                )

            return VALID_API_KEYS[api_key]

        # Test valid API key
        user_info = validate_api_key('api_key_123')
        assert user_info['user'] == 'alice'
        assert 'write' in user_info['permissions']

        # Test invalid cases
        with pytest.raises(HTTPException):
            validate_api_key('')

        with pytest.raises(HTTPException):
            validate_api_key('invalid_key')

    def test_permission_checking(self):
        """Test permission checking logic."""

        def check_permission(user_permissions, required_permission):
            if required_permission not in user_permissions:
                raise HTTPException(
                    status_code=403,
                    error='FORBIDDEN',
                    details={'message': f"Permission '{required_permission}' required"},
                )

        user_permissions = ['read', 'write']

        # Should pass
        check_permission(user_permissions, 'read')
        check_permission(user_permissions, 'write')

        # Should fail
        with pytest.raises(HTTPException):
            check_permission(user_permissions, 'admin')


class TestRateLimiting:
    """Test rate limiting functionality."""

    def test_simple_rate_limiter(self):
        """Test simple rate limiting logic."""
        import time
        from collections import defaultdict

        class SimpleRateLimiter:
            def __init__(self, max_requests=10, window_seconds=60):
                self.max_requests = max_requests
                self.window_seconds = window_seconds
                self.requests = defaultdict(list)

            def is_allowed(self, client_id):
                now = time.time()
                client_requests = self.requests[client_id]

                # Remove old requests outside the window
                self.requests[client_id] = [
                    req_time
                    for req_time in client_requests
                    if now - req_time < self.window_seconds
                ]

                # Check if within limit
                if len(self.requests[client_id]) >= self.max_requests:
                    return False

                # Add current request
                self.requests[client_id].append(now)
                return True

        limiter = SimpleRateLimiter(max_requests=3, window_seconds=1)

        # First 3 requests should be allowed
        assert limiter.is_allowed('client1') is True
        assert limiter.is_allowed('client1') is True
        assert limiter.is_allowed('client1') is True

        # 4th request should be denied
        assert limiter.is_allowed('client1') is False

        # Different client should be allowed
        assert limiter.is_allowed('client2') is True

    def test_rate_limiting_middleware_concept(self):
        """Test rate limiting middleware concept."""
        import time
        from collections import defaultdict

        class RateLimitingMiddleware:
            def __init__(self, app, max_requests=100, window_seconds=3600):
                self.app = app
                self.max_requests = max_requests
                self.window_seconds = window_seconds
                self.requests = defaultdict(list)

            def get_client_id(self, scope):
                # Simple client identification
                return scope.get('client', 'unknown')

            def is_rate_limited(self, client_id):
                now = time.time()
                client_requests = self.requests[client_id]

                # Clean old requests
                self.requests[client_id] = [
                    req_time
                    for req_time in client_requests
                    if now - req_time < self.window_seconds
                ]

                if len(self.requests[client_id]) >= self.max_requests:
                    return True

                self.requests[client_id].append(now)
                return False

            async def __call__(self, scope, protocol):
                client_id = self.get_client_id(scope)

                if self.is_rate_limited(client_id):
                    response = JSONResponse(
                        content={'error': 'Rate limit exceeded'}, status_code=429
                    )
                    await response(scope, protocol)
                    return

                await self.app(scope, protocol)

        # Test the middleware
        mock_app = AsyncMock()
        middleware = RateLimitingMiddleware(mock_app, max_requests=2, window_seconds=1)

        MagicMock()

        # First two requests should pass through
        assert middleware.is_rate_limited('test_client') is False
        assert middleware.is_rate_limited('test_client') is False

        # Third request should be rate limited
        assert middleware.is_rate_limited('test_client') is True


class TestSecurityHeaders:
    """Test security headers implementation."""

    def test_content_security_policy(self):
        """Test Content Security Policy header."""
        csp_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

        response = JSONResponse(
            content={'message': 'secure'},
            headers={'Content-Security-Policy': csp_policy},
        )

        headers = dict(response.raw_headers)
        assert headers.get('content-security-policy') == csp_policy

    def test_strict_transport_security(self):
        """Test Strict Transport Security header."""
        hsts_value = 'max-age=31536000; includeSubDomains; preload'

        response = JSONResponse(
            content={'message': 'secure'},
            headers={'Strict-Transport-Security': hsts_value},
        )

        headers = dict(response.raw_headers)
        assert headers.get('strict-transport-security') == hsts_value

    def test_x_frame_options(self):
        """Test X-Frame-Options header."""
        response = JSONResponse(
            content={'message': 'secure'}, headers={'X-Frame-Options': 'DENY'}
        )

        headers = dict(response.raw_headers)
        assert headers.get('x-frame-options') == 'DENY'

    def test_x_content_type_options(self):
        """Test X-Content-Type-Options header."""
        response = JSONResponse(
            content={'message': 'secure'}, headers={'X-Content-Type-Options': 'nosniff'}
        )

        headers = dict(response.raw_headers)
        assert headers.get('x-content-type-options') == 'nosniff'


if __name__ == '__main__':
    pytest.main([__file__])
