"""
Property-Based Tests for Error Handling System

This module contains property-based tests that verify correctness properties
of the error handling system across all possible inputs.

Properties tested:
- Property 4: Exception type correctness
- Property 5: Error response standardization
- Property 6: Error log completeness
- Property 7: Error response structure consistency
"""
import pytest
import logging
import json
from datetime import datetime
from unittest.mock import Mock, patch, MagicMock
from hypothesis import given, strategies as st, assume, settings
from hypothesis.strategies import composite
from fastapi import Request, FastAPI
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

from src.utils.errors import (
    AppException,
    BusinessException,
    SystemException,
    ErrorCode,
    InvalidParameterException,
    ResourceNotFoundException,
    ProjectNotFoundException,
    TaskNotFoundException,
    TaskTimeoutException,
    TaskQueueFullException,
    ModelNotFoundException,
    GenerationFailedException,
    StorageException,
    RateLimitExceededException,
    UnauthorizedException,
    ForbiddenException,
    ConflictException,
    AssetNotFoundException,
    CanvasNotFoundException,
    EpisodeNotFoundException,
    StoryboardNotFoundException,
    ProjectCreateFailedException,
    ProjectUpdateFailedException,
    ProjectDeleteFailedException,
    TaskExecutionFailedException,
    TaskCancelledException,
    ModelNotAvailableException,
    QuotaExceededException,
    InvalidPromptException,
    ProviderErrorException,
    UploadFailedException,
    DownloadFailedException,
    FileTooLargeException,
    InvalidFileTypeException,
)
from src.middlewares.error_handler import ErrorResponse, error_handler_middleware


# ============================================================================
# Hypothesis Strategies for Generating Test Data
# ============================================================================

@composite
def error_codes(draw):
    """Generate valid error codes"""
    return draw(st.sampled_from(list(ErrorCode)))


@composite
def error_messages(draw):
    """Generate error messages"""
    return draw(st.text(min_size=1, max_size=200))


@composite
def error_details(draw):
    """Generate error details dictionaries"""
    # Generate simple dictionaries with string keys and various value types
    keys = draw(st.lists(st.text(min_size=1, max_size=20), min_size=0, max_size=5, unique=True))
    values = []
    for _ in keys:
        value = draw(st.one_of(
            st.text(max_size=100),
            st.integers(),
            st.floats(allow_nan=False, allow_infinity=False),
            st.booleans(),
            st.none()
        ))
        values.append(value)
    return dict(zip(keys, values))


@composite
def business_exception_types(draw):
    """Generate business exception classes"""
    exception_classes = [
        InvalidParameterException,
        ResourceNotFoundException,
        ProjectNotFoundException,
        TaskNotFoundException,
        ModelNotFoundException,
        RateLimitExceededException,
        UnauthorizedException,
        ForbiddenException,
        ConflictException,
        AssetNotFoundException,
        CanvasNotFoundException,
        EpisodeNotFoundException,
        StoryboardNotFoundException,
        TaskCancelledException,
        ModelNotAvailableException,
        QuotaExceededException,
        InvalidPromptException,
    ]
    return draw(st.sampled_from(exception_classes))


@composite
def system_exception_types(draw):
    """Generate system exception classes"""
    exception_classes = [
        TaskTimeoutException,
        TaskQueueFullException,
        GenerationFailedException,
        StorageException,
        ProjectCreateFailedException,
        ProjectUpdateFailedException,
        ProjectDeleteFailedException,
        TaskExecutionFailedException,
        ProviderErrorException,
        UploadFailedException,
        DownloadFailedException,
    ]
    return draw(st.sampled_from(exception_classes))


@composite
def create_business_exception(draw, exception_class):
    """Create a business exception instance with appropriate parameters"""
    # Generate parameters based on exception type
    if exception_class == InvalidParameterException:
        field = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(field=field, reason=reason)
    
    elif exception_class == ResourceNotFoundException:
        resource_type = draw(st.text(min_size=1, max_size=50))
        resource_id = draw(st.text(min_size=1, max_size=50))
        return exception_class(resource_type=resource_type, resource_id=resource_id)
    
    elif exception_class in [ProjectNotFoundException, TaskNotFoundException, ModelNotFoundException,
                             AssetNotFoundException, CanvasNotFoundException, EpisodeNotFoundException,
                             StoryboardNotFoundException, TaskCancelledException]:
        id_value = draw(st.text(min_size=1, max_size=50))
        # Get the parameter name from the exception class
        if exception_class == ProjectNotFoundException:
            return exception_class(project_id=id_value)
        elif exception_class == TaskNotFoundException:
            return exception_class(task_id=id_value)
        elif exception_class == ModelNotFoundException:
            return exception_class(model_id=id_value)
        elif exception_class == AssetNotFoundException:
            return exception_class(asset_id=id_value)
        elif exception_class == CanvasNotFoundException:
            return exception_class(canvas_id=id_value)
        elif exception_class == EpisodeNotFoundException:
            return exception_class(episode_id=id_value)
        elif exception_class == StoryboardNotFoundException:
            return exception_class(storyboard_id=id_value)
        elif exception_class == TaskCancelledException:
            return exception_class(task_id=id_value)
    
    elif exception_class == RateLimitExceededException:
        limit = draw(st.integers(min_value=1, max_value=10000))
        window = draw(st.integers(min_value=1, max_value=3600))
        return exception_class(limit=limit, window=window)
    
    elif exception_class == UnauthorizedException:
        reason = draw(st.one_of(st.none(), st.text(min_size=1, max_size=100)))
        return exception_class(reason=reason)
    
    elif exception_class == ForbiddenException:
        reason = draw(st.one_of(st.none(), st.text(min_size=1, max_size=100)))
        return exception_class(reason=reason)
    
    elif exception_class == ConflictException:
        resource_type = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(resource_type=resource_type, reason=reason)
    
    elif exception_class == ModelNotAvailableException:
        model_id = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.one_of(st.none(), st.text(min_size=1, max_size=100)))
        return exception_class(model_id=model_id, reason=reason)
    
    elif exception_class == QuotaExceededException:
        resource = draw(st.text(min_size=1, max_size=50))
        limit = draw(st.integers(min_value=1, max_value=1000000))
        return exception_class(resource=resource, limit=limit)
    
    elif exception_class == InvalidPromptException:
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(reason=reason)
    
    # Default fallback
    return exception_class(project_id="test_id")


@composite
def create_system_exception(draw, exception_class):
    """Create a system exception instance with appropriate parameters"""
    if exception_class == TaskTimeoutException:
        task_id = draw(st.text(min_size=1, max_size=50))
        timeout = draw(st.integers(min_value=1, max_value=3600))
        return exception_class(task_id=task_id, timeout=timeout)
    
    elif exception_class == TaskQueueFullException:
        queue_size = draw(st.integers(min_value=1, max_value=10000))
        return exception_class(queue_size=queue_size)
    
    elif exception_class == GenerationFailedException:
        reason = draw(st.text(min_size=1, max_size=100))
        provider = draw(st.one_of(st.none(), st.text(min_size=1, max_size=50)))
        return exception_class(reason=reason, provider=provider)
    
    elif exception_class == StorageException:
        operation = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(operation=operation, reason=reason)
    
    elif exception_class == ProjectCreateFailedException:
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(reason=reason)
    
    elif exception_class in [ProjectUpdateFailedException, ProjectDeleteFailedException]:
        project_id = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(project_id=project_id, reason=reason)
    
    elif exception_class == TaskExecutionFailedException:
        task_id = draw(st.text(min_size=1, max_size=50))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(task_id=task_id, reason=reason)
    
    elif exception_class == ProviderErrorException:
        provider = draw(st.text(min_size=1, max_size=50))
        error_message = draw(st.text(min_size=1, max_size=100))
        return exception_class(provider=provider, error_message=error_message)
    
    elif exception_class == UploadFailedException:
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(reason=reason)
    
    elif exception_class == DownloadFailedException:
        url = draw(st.text(min_size=1, max_size=100))
        reason = draw(st.text(min_size=1, max_size=100))
        return exception_class(url=url, reason=reason)
    
    # Default fallback
    return exception_class(reason="test reason")


# ============================================================================
# Property 4: Exception Type Correctness
# ============================================================================

class TestProperty4ExceptionTypeCorrectness:
    """
    Property 4: 异常类型正确性
    
    验证错误条件抛出正确的异常类型
    Validates: Requirements 3.2
    """
    
    @given(exc=st.one_of([
        create_business_exception(InvalidParameterException),
        create_business_exception(ResourceNotFoundException),
        create_business_exception(ProjectNotFoundException),
        create_business_exception(TaskNotFoundException),
        create_business_exception(ModelNotFoundException),
        create_business_exception(RateLimitExceededException),
        create_business_exception(UnauthorizedException),
        create_business_exception(ForbiddenException),
        create_business_exception(ConflictException),
        create_business_exception(AssetNotFoundException),
        create_business_exception(CanvasNotFoundException),
        create_business_exception(EpisodeNotFoundException),
        create_business_exception(StoryboardNotFoundException),
        create_business_exception(TaskCancelledException),
        create_business_exception(ModelNotAvailableException),
        create_business_exception(QuotaExceededException),
        create_business_exception(InvalidPromptException),
    ]))
    @settings(max_examples=50, deadline=None)
    def test_business_exceptions_are_business_exception_type(self, exc):
        """
        Property: All business error conditions should throw BusinessException subclasses
        
        For any business exception class, instances should be BusinessException type
        """
        # Verify it's a BusinessException
        assert isinstance(exc, BusinessException), \
            f"{type(exc).__name__} should be a BusinessException"
        
        # Verify it's also an AppException
        assert isinstance(exc, AppException), \
            f"{type(exc).__name__} should be an AppException"
        
        # Verify status code is 4xx (except for rate limit which is 429)
        if isinstance(exc, RateLimitExceededException):
            assert exc.status_code == 429
        elif isinstance(exc, UnauthorizedException):
            assert exc.status_code == 401
        elif isinstance(exc, ForbiddenException):
            assert exc.status_code == 403
        elif isinstance(exc, ConflictException):
            assert exc.status_code == 409
        else:
            assert 400 <= exc.status_code < 500, \
                f"{type(exc).__name__} should have 4xx status code"
    
    @given(exc=st.one_of([
        create_system_exception(TaskTimeoutException),
        create_system_exception(TaskQueueFullException),
        create_system_exception(GenerationFailedException),
        create_system_exception(StorageException),
        create_system_exception(ProjectCreateFailedException),
        create_system_exception(ProjectUpdateFailedException),
        create_system_exception(ProjectDeleteFailedException),
        create_system_exception(TaskExecutionFailedException),
        create_system_exception(ProviderErrorException),
        create_system_exception(UploadFailedException),
        create_system_exception(DownloadFailedException),
    ]))
    @settings(max_examples=50, deadline=None)
    def test_system_exceptions_are_system_exception_type(self, exc):
        """
        Property: All system error conditions should throw SystemException subclasses
        
        For any system exception class, instances should be SystemException type
        """
        # Verify it's a SystemException
        assert isinstance(exc, SystemException), \
            f"{type(exc).__name__} should be a SystemException"
        
        # Verify it's also an AppException
        assert isinstance(exc, AppException), \
            f"{type(exc).__name__} should be an AppException"
        
        # Verify status code is 500
        assert exc.status_code == 500, \
            f"{type(exc).__name__} should have 500 status code"
    
    @given(
        code=error_codes(),
        message=error_messages(),
        details=error_details()
    )
    @settings(max_examples=100, deadline=None)
    def test_app_exception_preserves_error_information(self, code, message, details):
        """
        Property: AppException should preserve all error information
        
        For any error code, message, and details, the exception should store them correctly
        """
        exc = AppException(code=code, message=message, details=details)
        
        # Verify all information is preserved
        assert exc.code == code
        assert exc.message == message
        assert exc.details == details
        
        # Verify to_dict includes all information
        exc_dict = exc.to_dict()
        assert exc_dict["code"] == code.value
        assert exc_dict["message"] == message
        assert exc_dict["details"] == details


# ============================================================================
# Property 5: Error Response Standardization
# ============================================================================

class TestProperty5ErrorResponseStandardization:
    """
    Property 5: 错误响应标准化
    
    验证所有错误响应格式一致
    Validates: Requirements 3.3
    """
    
    @given(exc=st.one_of([
        create_business_exception(InvalidParameterException),
        create_business_exception(ResourceNotFoundException),
        create_business_exception(ProjectNotFoundException),
        create_business_exception(TaskNotFoundException),
        create_business_exception(ModelNotFoundException),
        create_business_exception(RateLimitExceededException),
        create_business_exception(UnauthorizedException),
        create_business_exception(ForbiddenException),
        create_business_exception(ConflictException),
        create_business_exception(AssetNotFoundException),
        create_business_exception(CanvasNotFoundException),
        create_business_exception(EpisodeNotFoundException),
        create_business_exception(StoryboardNotFoundException),
        create_business_exception(TaskCancelledException),
        create_business_exception(ModelNotAvailableException),
        create_business_exception(QuotaExceededException),
        create_business_exception(InvalidPromptException),
    ]))
    @settings(max_examples=50, deadline=None)
    async def test_business_exceptions_produce_standard_response(self, exc):
        """
        Property: All business exceptions should produce standardized JSON responses
        
        For any business exception, the error handler should convert it to standard format
        """
        # Create mock request
        app = FastAPI()
        request = Request(scope={
            "type": "http",
            "method": "GET",
            "path": "/test",
            "query_string": b"",
            "headers": [],
        })
        request.state.request_id = "test_request_id"
        request.state.timestamp = "2024-01-01T00:00:00Z"
        
        # Create mock call_next that raises the exception
        async def mock_call_next(req):
            raise exc
        
        # Call error handler middleware
        response = await error_handler_middleware(request, mock_call_next)
        
        # Verify response is JSONResponse
        assert isinstance(response, JSONResponse)
        
        # Parse response content
        content = json.loads(response.body.decode())
        
        # Verify standard format
        assert "code" in content
        assert "message" in content
        assert "details" in content
        assert "request_id" in content
        assert "timestamp" in content
        
        # Verify values match exception
        assert content["code"] == (exc.code.value if isinstance(exc.code, ErrorCode) else exc.code)
        assert content["message"] == exc.message
        assert content["details"] == exc.details
    
    @given(exc=st.one_of([
        create_system_exception(TaskTimeoutException),
        create_system_exception(TaskQueueFullException),
        create_system_exception(GenerationFailedException),
        create_system_exception(StorageException),
        create_system_exception(ProjectCreateFailedException),
        create_system_exception(ProjectUpdateFailedException),
        create_system_exception(ProjectDeleteFailedException),
        create_system_exception(TaskExecutionFailedException),
        create_system_exception(ProviderErrorException),
        create_system_exception(UploadFailedException),
        create_system_exception(DownloadFailedException),
    ]))
    @settings(max_examples=50, deadline=None)
    async def test_system_exceptions_produce_standard_response(self, exc):
        """
        Property: All system exceptions should produce standardized JSON responses
        
        For any system exception, the error handler should convert it to standard format
        """
        # Create mock request
        app = FastAPI()
        request = Request(scope={
            "type": "http",
            "method": "GET",
            "path": "/test",
            "query_string": b"",
            "headers": [],
        })
        request.state.request_id = "test_request_id"
        request.state.timestamp = "2024-01-01T00:00:00Z"
        
        # Create mock call_next that raises the exception
        async def mock_call_next(req):
            raise exc
        
        # Call error handler middleware
        response = await error_handler_middleware(request, mock_call_next)
        
        # Verify response is JSONResponse
        assert isinstance(response, JSONResponse)
        
        # Parse response content
        content = json.loads(response.body.decode())
        
        # Verify standard format
        assert "code" in content
        assert "message" in content
        assert "details" in content
        assert "request_id" in content
        assert "timestamp" in content
        
        # Verify values match exception
        assert content["code"] == (exc.code.value if isinstance(exc.code, ErrorCode) else exc.code)
        assert content["message"] == exc.message
        assert content["details"] == exc.details


# ============================================================================
# Property 6: Error Log Completeness
# ============================================================================

class TestProperty6ErrorLogCompleteness:
    """
    Property 6: 错误日志完整性
    
    验证错误日志包含必要信息
    Validates: Requirements 3.4
    """
    
    @given(exc=st.one_of([
        create_business_exception(InvalidParameterException),
        create_business_exception(ResourceNotFoundException),
        create_business_exception(ProjectNotFoundException),
        create_business_exception(TaskNotFoundException),
        create_business_exception(ModelNotFoundException),
        create_business_exception(RateLimitExceededException),
        create_business_exception(UnauthorizedException),
        create_business_exception(ForbiddenException),
        create_business_exception(ConflictException),
        create_business_exception(AssetNotFoundException),
        create_business_exception(CanvasNotFoundException),
        create_business_exception(EpisodeNotFoundException),
        create_business_exception(StoryboardNotFoundException),
        create_business_exception(TaskCancelledException),
        create_business_exception(ModelNotAvailableException),
        create_business_exception(QuotaExceededException),
        create_business_exception(InvalidPromptException),
    ]))
    @settings(max_examples=50, deadline=None)
    async def test_business_exceptions_log_with_warning_level(self, exc):
        """
        Property: Business exceptions should be logged with WARNING level
        
        For any business exception, the log should use WARNING severity
        """
        # Create mock request
        request = Request(scope={
            "type": "http",
            "method": "GET",
            "path": "/test",
            "query_string": b"",
            "headers": [],
        })
        request.state.request_id = "test_request_id"
        request.state.timestamp = "2024-01-01T00:00:00Z"
        
        # Create mock call_next that raises the exception
        async def mock_call_next(req):
            raise exc
        
        # Mock logger to capture log calls
        with patch('src.middlewares.error_handler.logger') as mock_logger:
            # Call error handler middleware
            response = await error_handler_middleware(request, mock_call_next)
            
            # Verify logger.log was called with WARNING level
            mock_logger.log.assert_called_once()
            call_args = mock_logger.log.call_args
            
            # First argument should be WARNING level
            assert call_args[0][0] == logging.WARNING, \
                f"Business exception should log with WARNING level, got {call_args[0][0]}"
            
            # Verify log includes necessary context
            extra = call_args[1].get('extra', {})
            assert 'request_id' in extra
            assert 'timestamp' in extra
            assert 'path' in extra
            assert 'method' in extra
            assert 'error_code' in extra
            assert 'details' in extra
            assert 'exception_type' in extra
    
    @given(exc=st.one_of([
        create_system_exception(TaskTimeoutException),
        create_system_exception(TaskQueueFullException),
        create_system_exception(GenerationFailedException),
        create_system_exception(StorageException),
        create_system_exception(ProjectCreateFailedException),
        create_system_exception(ProjectUpdateFailedException),
        create_system_exception(ProjectDeleteFailedException),
        create_system_exception(TaskExecutionFailedException),
        create_system_exception(ProviderErrorException),
        create_system_exception(UploadFailedException),
        create_system_exception(DownloadFailedException),
    ]))
    @settings(max_examples=50, deadline=None)
    async def test_system_exceptions_log_with_error_level(self, exc):
        """
        Property: System exceptions should be logged with ERROR level
        
        For any system exception, the log should use ERROR severity
        """
        # Create mock request
        request = Request(scope={
            "type": "http",
            "method": "GET",
            "path": "/test",
            "query_string": b"",
            "headers": [],
        })
        request.state.request_id = "test_request_id"
        request.state.timestamp = "2024-01-01T00:00:00Z"
        
        # Create mock call_next that raises the exception
        async def mock_call_next(req):
            raise exc
        
        # Mock logger to capture log calls
        with patch('src.middlewares.error_handler.logger') as mock_logger:
            # Call error handler middleware
            response = await error_handler_middleware(request, mock_call_next)
            
            # Verify logger.log was called with ERROR level
            mock_logger.log.assert_called_once()
            call_args = mock_logger.log.call_args
            
            # First argument should be ERROR level
            assert call_args[0][0] == logging.ERROR, \
                f"System exception should log with ERROR level, got {call_args[0][0]}"
            
            # Verify log includes necessary context
            extra = call_args[1].get('extra', {})
            assert 'request_id' in extra
            assert 'timestamp' in extra
            assert 'path' in extra
            assert 'method' in extra
            assert 'error_code' in extra
            assert 'details' in extra
            assert 'exception_type' in extra
            
            # Verify exc_info is True for system exceptions (includes stack trace)
            assert call_args[1].get('exc_info') == True, \
                "System exceptions should include stack trace (exc_info=True)"


# ============================================================================
# Property 7: Error Response Structure Consistency
# ============================================================================

class TestProperty7ErrorResponseStructureConsistency:
    """
    Property 7: 错误响应结构一致性
    
    验证错误响应JSON结构
    Validates: Requirements 3.5
    """
    
    @given(
        code=error_codes(),
        message=error_messages(),
        details=error_details()
    )
    @settings(max_examples=100, deadline=None)
    def test_error_response_has_consistent_structure(self, code, message, details):
        """
        Property: All error responses should have consistent JSON structure
        
        For any error code, message, and details, the response structure should be identical
        """
        # Create ErrorResponse
        error_response = ErrorResponse(
            code=code.value,
            message=message,
            details=details,
            request_id="test_request_id",
            timestamp="2024-01-01T00:00:00Z"
        )
        
        # Convert to dict
        response_dict = error_response.to_dict()
        
        # Verify structure has exactly these keys
        expected_keys = {"code", "message", "details", "request_id", "timestamp"}
        assert set(response_dict.keys()) == expected_keys, \
            f"Response should have exactly {expected_keys}, got {set(response_dict.keys())}"
        
        # Verify types
        assert isinstance(response_dict["code"], str)
        assert isinstance(response_dict["message"], str)
        assert isinstance(response_dict["details"], dict)
        assert isinstance(response_dict["request_id"], str)
        assert isinstance(response_dict["timestamp"], str)
        
        # Verify values match input
        assert response_dict["code"] == code.value
        assert response_dict["message"] == message
        assert response_dict["details"] == details
    
    @given(exc=st.one_of([
        # Business exceptions
        create_business_exception(InvalidParameterException),
        create_business_exception(ResourceNotFoundException),
        create_business_exception(ProjectNotFoundException),
        create_business_exception(TaskNotFoundException),
        create_business_exception(ModelNotFoundException),
        create_business_exception(RateLimitExceededException),
        create_business_exception(UnauthorizedException),
        create_business_exception(ForbiddenException),
        create_business_exception(ConflictException),
        create_business_exception(AssetNotFoundException),
        create_business_exception(CanvasNotFoundException),
        create_business_exception(EpisodeNotFoundException),
        create_business_exception(StoryboardNotFoundException),
        create_business_exception(TaskCancelledException),
        create_business_exception(ModelNotAvailableException),
        create_business_exception(QuotaExceededException),
        create_business_exception(InvalidPromptException),
        # System exceptions
        create_system_exception(TaskTimeoutException),
        create_system_exception(TaskQueueFullException),
        create_system_exception(GenerationFailedException),
        create_system_exception(StorageException),
        create_system_exception(ProjectCreateFailedException),
        create_system_exception(ProjectUpdateFailedException),
        create_system_exception(ProjectDeleteFailedException),
        create_system_exception(TaskExecutionFailedException),
        create_system_exception(ProviderErrorException),
        create_system_exception(UploadFailedException),
        create_system_exception(DownloadFailedException),
    ]))
    @settings(max_examples=100, deadline=None)
    async def test_all_exceptions_produce_same_response_structure(self, exc):
        """
        Property: All exception types should produce responses with identical structure
        
        For any exception type, the response structure should be consistent
        """
        # Create mock request
        request = Request(scope={
            "type": "http",
            "method": "GET",
            "path": "/test",
            "query_string": b"",
            "headers": [],
        })
        request.state.request_id = "test_request_id"
        request.state.timestamp = "2024-01-01T00:00:00Z"
        
        # Create mock call_next that raises the exception
        async def mock_call_next(req):
            raise exc
        
        # Call error handler middleware
        response = await error_handler_middleware(request, mock_call_next)
        
        # Parse response content
        content = json.loads(response.body.decode())
        
        # Verify structure is consistent
        expected_keys = {"code", "message", "details", "request_id", "timestamp"}
        assert set(content.keys()) == expected_keys, \
            f"All responses should have structure {expected_keys}, got {set(content.keys())}"
        
        # Verify types are consistent
        assert isinstance(content["code"], str)
        assert isinstance(content["message"], str)
        assert isinstance(content["details"], dict)
        assert isinstance(content["request_id"], str)
        assert isinstance(content["timestamp"], str)
        
        # Verify timestamp is ISO format
        try:
            datetime.fromisoformat(content["timestamp"].replace("Z", "+00:00"))
        except ValueError:
            pytest.fail(f"Timestamp should be ISO format, got {content['timestamp']}")


if __name__ == "__main__":
    pytest.main([__file__, "-v", "--tb=short"])
