from dataclasses import dataclass
from logging import getLogger
from typing import Optional
from unittest import TestCase

import yaml
from httpmessageprocessorutil.automapped import AutoMappedMessageProcessor
from samplehttpmodule.messagehandlerbuilder import (
    DependencyInjectionSampleMessageHandlerBuilder,
    SampleMessageHandlerBuilder
)
from samplehttpmodule.route import add_descriptors
from simplehttprouter import Route

import edpopenapibuilder.builder.registry as _builder_registry
from edpopenapibuilder.builder.descriptors import path_descriptor
from edpopenapibuilder.builder.helper import build_from_message_handler_builder
from edpopenapibuilder.builder.impl import OpenApiModelBuilderImpl
from edpopenapibuilder.model import ServerModel, ServerVariableModel

_LOGGER = getLogger(__name__)


class TestBuilderHelper(TestCase):
    def setUp(self):
        add_descriptors()
        _builder_registry._DEFAULT_BUILDER = None

    def _build_yaml_from_di(self) -> dict:
        message_handler_builder = DependencyInjectionSampleMessageHandlerBuilder()
        builder = build_from_message_handler_builder(message_handler_builder)
        server = ServerModel(url='http://example.com', description='server description')
        server.add_variable('test_var', ServerVariableModel(description='Test Variable', default='test'))
        builder.get_model().add_server(server)
        result = builder.build_open_api_yaml()
        # _LOGGER.info(result)
        return yaml.safe_load(result)

    def _build_yaml_from_routes(self) -> dict:
        message_handler_builder = SampleMessageHandlerBuilder()
        builder = build_from_message_handler_builder(message_handler_builder)
        result = builder.build_open_api_yaml()
        # _LOGGER.info(result)
        return yaml.safe_load(result)

    # --- DI message handler builder tests ---

    def test_di_openapi_metadata(self):
        data = self._build_yaml_from_di()
        self.assertEqual(data['openapi'], '3.0.0')
        self.assertEqual(data['info']['title'], 'Open API Definition')

    def test_di_server(self):
        data = self._build_yaml_from_di()
        self.assertEqual(len(data['servers']), 1)
        self.assertEqual(data['servers'][0]['url'], 'http://example.com')
        self.assertEqual(data['servers'][0]['description'], 'server description')
        self.assertIn('test_var', data['servers'][0]['variables'])
        self.assertEqual(data['servers'][0]['variables']['test_var']['default'], 'test')

    def test_di_all_paths_present(self):
        paths = self._build_yaml_from_di()['paths']
        for expected in ['/oas/yaml', '/build', '/allTypes', '/allTypes/{integer_param}',
                         '/build/{param}', '/complex', '/listOfObject', '/listAliasOutput',
                         '/unionRandom', '/test-build', '/test-build/{param}']:
            self.assertIn(expected, paths)

    def test_di_all_types_get_descriptor_applied(self):
        all_types_get = self._build_yaml_from_di()['paths']['/allTypes']['get']
        self.assertEqual(all_types_get['summary'], 'All Types Route summary')
        self.assertEqual(all_types_get['tags'], ['alltypes', 'described'])

    def test_di_all_types_get_response_ref(self):
        schema = self._build_yaml_from_di()['paths']['/allTypes']['get']['responses']['200']['content']['application/json']['schema']
        self.assertEqual(schema, {'$ref': '#/components/schemas/AllTypesDto'})

    def test_di_all_types_post_request_body_ref(self):
        schema = self._build_yaml_from_di()['paths']['/allTypes']['post']['requestBody']['content']['application/json']['schema']
        self.assertEqual(schema, {'$ref': '#/components/schemas/AllTypesDto'})

    def test_di_complex_post_refs(self):
        complex_post = self._build_yaml_from_di()['paths']['/complex']['post']
        self.assertEqual(
            complex_post['requestBody']['content']['application/json']['schema'],
            {'$ref': '#/components/schemas/ComplexDto'},
        )
        self.assertEqual(
            complex_post['responses']['200']['content']['application/json']['schema'],
            {'$ref': '#/components/schemas/ComplexDto'},
        )

    def test_di_component_schemas_present(self):
        schemas = self._build_yaml_from_di()['components']['schemas']
        for name in ['AllTypesDto', 'ComplexDto', 'SimpleDto', 'ListResponse']:
            self.assertIn(name, schemas)
        for name in ['ArrayOfSimpleDto', 'ArrayOfListResponse']:
            self.assertNotIn(name, schemas)

    def test_di_all_types_dto_schema(self):
        schema = self._build_yaml_from_di()['components']['schemas']['AllTypesDto']
        self.assertEqual(schema['type'], 'object')
        self.assertCountEqual(
            schema['required'],
            ['integer_param', 'string_param', 'float_param', 'boolean_param', 'optional_param'],
        )

    def test_di_complex_dto_nested_array_ref(self):
        schemas = self._build_yaml_from_di()['components']['schemas']
        self.assertEqual(
            schemas['ComplexDto']['properties']['children'],
            {'type': 'array', 'items': {'$ref': '#/components/schemas/SimpleDto'}},
        )

    # --- Plain route builder tests ---

    def test_routes_openapi_metadata(self):
        data = self._build_yaml_from_routes()
        self.assertEqual(data['openapi'], '3.0.0')

    def test_routes_no_server(self):
        data = self._build_yaml_from_routes()
        self.assertNotIn('servers', data)

    def test_routes_all_paths_present(self):
        paths = self._build_yaml_from_routes()['paths']
        for expected in ['/build', '/allTypes', '/allTypes/{integer_param}',
                         '/build/{param}', '/complex', '/listOfObject', '/listAliasOutput',
                         '/unionRandom', '/test-build', '/test-build/{param}']:
            self.assertIn(expected, paths)

    def test_routes_all_types_get_descriptor_applied(self):
        all_types_get = self._build_yaml_from_routes()['paths']['/allTypes']['get']
        self.assertEqual(all_types_get['summary'], 'All Types Route summary')
        self.assertEqual(all_types_get['tags'], ['alltypes', 'described'])

    def test_routes_build_get_descriptor_applied(self):
        build_get = self._build_yaml_from_routes()['paths']['/build']['get']
        self.assertEqual(build_get['summary'], 'Builder Route summary')
        self.assertEqual(build_get['tags'], ['builder', 'described'])

    def test_routes_all_types_get_response_ref(self):
        schema = self._build_yaml_from_routes()['paths']['/allTypes']['get']['responses']['200']['content']['application/json']['schema']
        self.assertEqual(schema, {'$ref': '#/components/schemas/AllTypesDto'})

    def test_routes_complex_post_refs(self):
        complex_post = self._build_yaml_from_routes()['paths']['/complex']['post']
        self.assertEqual(
            complex_post['requestBody']['content']['application/json']['schema'],
            {'$ref': '#/components/schemas/ComplexDto'},
        )

    def test_routes_list_of_object_response_ref(self):
        schema = self._build_yaml_from_routes()['paths']['/listOfObject']['get']['responses']['200']['content']['application/json']['schema']
        self.assertEqual(schema, {'type': 'array', 'items': {'$ref': '#/components/schemas/ListResponse'}})

    def test_routes_component_schemas_present(self):
        schemas = self._build_yaml_from_routes()['components']['schemas']
        for name in ['AllTypesDto', 'ComplexDto', 'SimpleDto', 'ListResponse']:
            self.assertIn(name, schemas)
        for name in ['ArrayOfSimpleDto', 'ArrayOfListResponse']:
            self.assertNotIn(name, schemas)

    def test_routes_complex_dto_nested_array_ref(self):
        schemas = self._build_yaml_from_routes()['components']['schemas']
        self.assertEqual(
            schemas['ComplexDto']['properties']['children'],
            {'type': 'array', 'items': {'$ref': '#/components/schemas/SimpleDto'}},
        )


@dataclass
class _ArchiveItemRequest:
    id: Optional[str] = None
    tenant_id: Optional[str] = None


class _ArchiveItemInteractor:
    def execute(self, request: _ArchiveItemRequest) -> None:
        pass


class _CustomProcessor(AutoMappedMessageProcessor):
    """Simulates a processor subclass with custom init parameters."""
    def __init__(self, request_type, interactor, extra_mapping=None, exception_map=None):
        super().__init__(
            request_type=request_type,
            interactor=interactor,
            extra_mapping=extra_mapping,
            exception_map=exception_map,
        )


def _update_secured(_builder, _params, path):
    path.tags.append('secured')
    return path


class TestAutoMappedProcessorExtraMappingExtraction(TestCase):
    def setUp(self):
        _builder_registry._DEFAULT_BUILDER = None

    def _build_with_extra_mapping(self, processor, route, with_descriptor=False):
        builder = OpenApiModelBuilderImpl()
        if with_descriptor:
            builder.descriptors.add_processor_descriptor(
                type(processor),
                path_descriptor(update=_update_secured),
            )
        builder.add_processor_path(route=route, processor=processor)
        return yaml.safe_load(builder.build_open_api_yaml())

    def test_extra_mapping_header_parameter(self):
        processor = AutoMappedMessageProcessor(
            _ArchiveItemRequest,
            _ArchiveItemInteractor(),
            extra_mapping={
                "tenant_id": {
                    "name": "X-Tenant-Id",
                    "location": "header",
                }
            },
        )
        route = Route("POST", "/items/{id}/archive")
        result = self._build_with_extra_mapping(processor, route)

        path = result['paths']['/items/{id}/archive']['post']
        header_params = [
            p for p in path.get('parameters', [])
            if p.get('in') == 'header'
        ]
        self.assertEqual(len(header_params), 1)
        self.assertEqual(header_params[0]['name'], 'X-Tenant-Id')
        self.assertTrue(header_params[0]['required'])

    def test_extra_mapping_with_processor_subclass_and_descriptor(self):
        processor = _CustomProcessor(
            _ArchiveItemRequest,
            _ArchiveItemInteractor(),
            extra_mapping={
                "tenant_id": {
                    "name": "X-Tenant-Id",
                    "location": "header",
                }
            },
            exception_map={ValueError: 400},
        )
        route = Route("POST", "/items/{id}/archive")
        result = self._build_with_extra_mapping(processor, route, with_descriptor=True)

        path = result['paths']['/items/{id}/archive']['post']
        # Header parameter from extra_mapping
        header_params = [
            p for p in path.get('parameters', [])
            if p.get('in') == 'header'
        ]
        self.assertEqual(len(header_params), 1)
        self.assertEqual(header_params[0]['name'], 'X-Tenant-Id')
        self.assertTrue(header_params[0]['required'])
        # Exception map produces 400 response
        self.assertIn('400', path['responses'])
        # Descriptor applied the secured tag
        self.assertIn('secured', path.get('tags', []))
        # All fields covered by path + extra_mapping — no request body
        self.assertNotIn('requestBody', path)

    def test_extra_mapping_with_custom_build_params_descriptor(self):
        """Verify extra_mapping is extracted even when a descriptor overrides build_params."""
        def custom_build_params(builder, route, processor):
            from edpopenapibuilder.builder.dto import build_params as _build_params
            params = _build_params(route=route, processor_type=type(processor))
            params["interactor_type"] = type(processor.interactor)
            params["request_type"] = processor.parser.request_type
            # Intentionally does NOT set extra_mapping or exception_map
            return params

        processor = _CustomProcessor(
            _ArchiveItemRequest,
            _ArchiveItemInteractor(),
            extra_mapping={
                "tenant_id": {
                    "name": "X-Tenant-Id",
                    "location": "header",
                }
            },
            exception_map={ValueError: 400},
        )
        builder = OpenApiModelBuilderImpl()
        builder.descriptors.add_processor_descriptor(
            _CustomProcessor,
            path_descriptor(
                build_params=custom_build_params,
                update=_update_secured,
            ),
        )
        route = Route("POST", "/items/{id}/archive")
        builder.add_processor_path(route=route, processor=processor)
        result = yaml.safe_load(builder.build_open_api_yaml())

        path = result['paths']['/items/{id}/archive']['post']
        header_params = [
            p for p in path.get('parameters', [])
            if p.get('in') == 'header'
        ]
        self.assertEqual(len(header_params), 1)
        self.assertEqual(header_params[0]['name'], 'X-Tenant-Id')
        self.assertIn('400', path['responses'])
        self.assertIn('secured', path.get('tags', []))

    def test_exception_map_extracted(self):
        processor = AutoMappedMessageProcessor(
            _ArchiveItemRequest,
            _ArchiveItemInteractor(),
            exception_map={ValueError: 400},
        )
        route = Route("POST", "/test-exception-map")
        result = self._build_with_extra_mapping(processor, route)

        responses = result['paths']['/test-exception-map']['post']['responses']
        self.assertIn('400', responses)
