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

import samplehttpmodule
import yaml
from dihttpmessageprocessor import AutoMappedDiRouterProcessorBuilder
from edpdependencyinjection import DependencyLoader
from samplehttpmodule.route import add_descriptors

import edpopenapibuilder.injection
from edpopenapibuilder.injection.interactor import (
    DependencyInjectionApiYamlBuilderInteractor,
)
from edpopenapibuilder.interactor.dto import AutoOpenApiYamlBuilderRequest

_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"])
