from logging import getLogger
from typing import Dict, Optional
from unittest import TestCase

import yaml
from dependencyinjection import DependencyLoader
from dihttpmessageprocessor import AutoMappedDiRouterProcessorBuilder

import edpopenapibuilder.injection
import samplehttpmodule
from edpopenapibuilder.injection.interactor import DependencyInjectionApiYamlBuilderInteractor
from edpopenapibuilder.interactor.dto import AutoOpenApiYamlBuilderRequest
from samplehttpmodule.route import add_descriptors

_LOGGER = getLogger(__name__)


class TestDependencyInjectionApiYamlBuilderInteractor(TestCase):
    def setUp(self):
        add_descriptors()
        self.dependencies_loader = DependencyLoader()
        self.dependencies_loader.load_modules(
            modules=[
                samplehttpmodule,
                edpopenapibuilder.injection
            ],
        )

    def _get_router_builder(self, route_definition: Optional[Dict] = None) -> AutoMappedDiRouterProcessorBuilder:
        return AutoMappedDiRouterProcessorBuilder(
            dependency_context=self.dependencies_loader.build_context(),
            route_definition=route_definition or {},
        )

    def _execute(self, host: Optional[str] = None) -> dict:
        router_processor_builder = self._get_router_builder()
        interactor = DependencyInjectionApiYamlBuilderInteractor(
            router_processor_builder=router_processor_builder
        )
        result = interactor.execute(request=AutoOpenApiYamlBuilderRequest(host=host))
        # _LOGGER.info(f"\n{result}")
        return yaml.safe_load(result)

    def test_openapi_metadata(self):
        data = self._execute()
        self.assertEqual(data['openapi'], '3.0.0')
        self.assertEqual(data['info']['title'], 'Open API Definition')
        self.assertEqual(data['info']['version'], '1.0.0')

    def test_no_server_when_no_host(self):
        data = self._execute()
        self.assertNotIn('servers', data)

    def test_server_added_when_host_provided(self):
        data = self._execute(host='http://example.com')
        self.assertEqual(len(data['servers']), 1)
        self.assertEqual(data['servers'][0]['url'], 'http://example.com')

    def test_all_paths_present(self):
        paths = self._execute()['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_all_types_get_descriptor_applied(self):
        all_types_get = self._execute()['paths']['/allTypes']['get']
        self.assertEqual(all_types_get['summary'], 'All Types Route summary')
        self.assertEqual(all_types_get['description'], 'All Types Route description')
        self.assertEqual(all_types_get['tags'], ['alltypes', 'described'])

    def test_all_types_get_query_parameters(self):
        all_types_get = self._execute()['paths']['/allTypes']['get']
        param_names = [p['name'] for p in all_types_get['parameters']]
        self.assertIn('integer_param', param_names)
        self.assertIn('string_param', param_names)
        self.assertIn('float_param', param_names)
        self.assertIn('boolean_param', param_names)
        self.assertIn('optional_param', param_names)
        self.assertIn('parameter_with_default', param_names)

    def test_all_types_get_required_vs_optional_params(self):
        params = {p['name']: p for p in self._execute()['paths']['/allTypes']['get']['parameters']}
        self.assertTrue(params['integer_param']['required'])
        self.assertTrue(params['string_param']['required'])
        self.assertFalse(params['optional_param']['required'])
        self.assertFalse(params['parameter_with_default']['required'])
        self.assertEqual(params['parameter_with_default']['schema']['default'], 'value')

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

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

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

    def test_build_param_path_parameter(self):
        params = self._execute()['paths']['/build/{param}']['get']['parameters']
        path_params = [p for p in params if p['in'] == 'path']
        self.assertEqual(len(path_params), 1)
        self.assertEqual(path_params[0]['name'], 'param')
        self.assertTrue(path_params[0]['required'])

    def test_complex_post_request_and_response_ref(self):
        complex_post = self._execute()['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_list_of_object_response_ref(self):
        schema = self._execute()['paths']['/listOfObject']['get']['responses']['200']['content']['application/json']['schema']
        self.assertEqual(schema, {'type': 'array', 'items': {'$ref': '#/components/schemas/ListResponse'}})

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

    def test_all_types_dto_schema(self):
        schema = self._execute()['components']['schemas']['AllTypesDto']
        self.assertEqual(schema['type'], 'object')
        self.assertCountEqual(
            schema['required'],
            ['integer_param', 'string_param', 'float_param', 'boolean_param', 'optional_param'],
        )
        self.assertEqual(schema['properties']['integer_param'], {'type': 'integer'})
        self.assertEqual(schema['properties']['string_param'], {'type': 'string'})
        self.assertEqual(schema['properties']['float_param'], {'type': 'number'})
        self.assertEqual(schema['properties']['boolean_param'], {'type': 'boolean'})

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

    def test_simple_dto_schema(self):
        schema = self._execute()['components']['schemas']['SimpleDto']
        self.assertEqual(schema['type'], 'object')
        self.assertEqual(schema['properties']['id'], {'type': 'integer'})
        self.assertEqual(schema['properties']['name'], {'type': 'string'})
        self.assertCountEqual(schema['required'], ['id', 'name'])

    def test_list_response_schema(self):
        schema = self._execute()['components']['schemas']['ListResponse']
        self.assertEqual(schema['type'], 'object')
        self.assertEqual(schema['properties']['id'], {'type': 'integer'})
        self.assertEqual(schema['properties']['name'], {'type': 'string'})
        self.assertCountEqual(schema['required'], ['id', 'name'])
