from unittest import TestCase
from unittest.mock import MagicMock

from simplehttprouter import Route

from edpopenapibuilder.builder.descriptors import (
    Descriptors,
    PathModelDescriptor,
    PayloadDescriptor,
    path_descriptor,
    payload_descriptor,
)
from edpopenapibuilder.builder.dto import build_params
from edpopenapibuilder.model import PathModel, SchemaModel


# --- Stub types for inheritance tests ---

class _BaseInteractor:
    pass


class _ChildInteractor(_BaseInteractor):
    pass


class _GrandchildInteractor(_ChildInteractor):
    pass


class _UnrelatedInteractor:
    pass


class _BaseProcessor:
    pass


class _ChildProcessor(_BaseProcessor):
    pass


class _BaseDto:
    pass


class _ChildDto(_BaseDto):
    pass


class _UnrelatedDto:
    pass


# --- PathModelDescriptor ---

class TestPathModelDescriptor(TestCase):
    def test_is_available_initially_true(self):
        desc = PathModelDescriptor()
        self.assertTrue(desc.is_available())

    def test_build_params_calls_callback(self):
        mock_callback = MagicMock(return_value={"route": None})
        desc = PathModelDescriptor(build_params_callback=mock_callback)
        builder = MagicMock()
        route = Route("GET", "/test")

        result = desc.build_params(builder, route, None)

        mock_callback.assert_called_once_with(builder, route, None)
        self.assertEqual(result, {"route": None})

    def test_build_params_falls_back_to_builder(self):
        desc = PathModelDescriptor()
        builder = MagicMock()
        route = Route("GET", "/test")
        builder.build_processor_params.return_value = {"route": route}

        result = desc.build_params(builder, route, None)

        builder.build_processor_params.assert_called_once_with(route=route, processor=None)

    def test_build_params_restores_available_on_success(self):
        desc = PathModelDescriptor(build_params_callback=MagicMock(return_value={}))
        desc.build_params(MagicMock(), Route("GET", "/x"), None)
        self.assertTrue(desc.is_available())

    def test_build_params_restores_available_on_error(self):
        desc = PathModelDescriptor(build_params_callback=MagicMock(side_effect=ValueError))
        with self.assertRaises(ValueError):
            desc.build_params(MagicMock(), Route("GET", "/x"), None)
        self.assertTrue(desc.is_available())

    def test_create_path_calls_callback(self):
        mock_callback = MagicMock(return_value=PathModel())
        desc = PathModelDescriptor(create_callback=mock_callback)
        builder = MagicMock()
        params = build_params(route=Route("GET", "/test"))

        result = desc.create_path(builder, params)

        mock_callback.assert_called_once_with(builder, params)
        self.assertIsInstance(result, PathModel)

    def test_create_path_falls_back_to_builder(self):
        desc = PathModelDescriptor()
        builder = MagicMock()
        params = build_params(route=Route("GET", "/test"))
        builder.add_interactor_path.return_value = PathModel()

        desc.create_path(builder, params)

        builder.add_interactor_path.assert_called_once_with(params)

    def test_create_path_restores_available(self):
        desc = PathModelDescriptor(create_callback=MagicMock(return_value=PathModel()))
        desc.create_path(MagicMock(), build_params(route=Route("GET", "/x")))
        self.assertTrue(desc.is_available())

    def test_update_path_calls_callback(self):
        path = PathModel()
        mock_callback = MagicMock(return_value=path)
        desc = PathModelDescriptor(update_callback=mock_callback)
        builder = MagicMock()
        params = build_params(route=Route("GET", "/test"))

        result = desc.update_path(builder, params, path)

        mock_callback.assert_called_once_with(builder, params, path)
        self.assertIs(result, path)

    def test_update_path_returns_path_unchanged_without_callback(self):
        desc = PathModelDescriptor()
        path = PathModel()

        result = desc.update_path(MagicMock(), build_params(route=Route("GET", "/x")), path)

        self.assertIs(result, path)

    def test_update_path_restores_available(self):
        desc = PathModelDescriptor(update_callback=MagicMock(return_value=PathModel()))
        desc.update_path(MagicMock(), build_params(route=Route("GET", "/x")), PathModel())
        self.assertTrue(desc.is_available())

    def test_reentrancy_guard_disables_during_callback(self):
        captured = {}

        def capture_availability(builder, params):
            captured["available"] = desc.is_available()
            return PathModel()

        desc = PathModelDescriptor(create_callback=capture_availability)
        desc.create_path(MagicMock(), build_params(route=Route("GET", "/x")))

        self.assertFalse(captured["available"])
        self.assertTrue(desc.is_available())


# --- path_descriptor factory ---

class TestPathDescriptorFactory(TestCase):
    def test_returns_path_model_descriptor(self):
        desc = path_descriptor()
        self.assertIsInstance(desc, PathModelDescriptor)

    def test_passes_callbacks(self):
        bp = MagicMock()
        cr = MagicMock()
        up = MagicMock()
        desc = path_descriptor(build_params=bp, create=cr, update=up)
        self.assertIs(desc._build_params_callback, bp)
        self.assertIs(desc._create_callback, cr)
        self.assertIs(desc._update_callback, up)


# --- PayloadDescriptor ---

class TestPayloadDescriptor(TestCase):
    def test_is_available_initially_true(self):
        desc = PayloadDescriptor()
        self.assertTrue(desc.is_available())

    def test_create_object_calls_callback(self):
        mock_callback = MagicMock(return_value=SchemaModel())
        desc = PayloadDescriptor(create_callback=mock_callback)
        builder = MagicMock()
        params = build_params(route=Route("POST", "/test"), request_type=str)

        result = desc.create_object(builder, params)

        mock_callback.assert_called_once_with(builder, params, None)
        self.assertIsInstance(result, SchemaModel)

    def test_create_object_falls_back_to_builder(self):
        desc = PayloadDescriptor()
        builder = MagicMock()
        builder.build_schema_model.return_value = SchemaModel()
        params = build_params(route=Route("POST", "/test"), request_type=str)

        desc.create_object(builder, params)

        builder.build_schema_model.assert_called_once()

    def test_create_object_restores_available(self):
        desc = PayloadDescriptor(create_callback=MagicMock(return_value=SchemaModel()))
        desc.create_object(MagicMock(), build_params(route=Route("POST", "/x"), request_type=str))
        self.assertTrue(desc.is_available())

    def test_create_object_restores_available_on_error(self):
        desc = PayloadDescriptor(create_callback=MagicMock(side_effect=ValueError))
        with self.assertRaises(ValueError):
            desc.create_object(MagicMock(), build_params(route=Route("POST", "/x"), request_type=str))
        self.assertTrue(desc.is_available())

    def test_update_object_calls_callback(self):
        schema = SchemaModel()
        mock_callback = MagicMock(return_value=schema)
        desc = PayloadDescriptor(update_callback=mock_callback)
        builder = MagicMock()
        params = build_params(route=Route("POST", "/test"))

        result = desc.update_object(builder, params, schema)

        mock_callback.assert_called_once_with(builder, params, schema, None)
        self.assertIs(result, schema)

    def test_update_object_returns_schema_unchanged_without_callback(self):
        desc = PayloadDescriptor()
        schema = SchemaModel()

        result = desc.update_object(MagicMock(), build_params(route=Route("POST", "/x")), schema)

        self.assertIs(result, schema)

    def test_reentrancy_guard_disables_during_callback(self):
        captured = {}

        def capture_availability(builder, params, path_model):
            captured["available"] = desc.is_available()
            return SchemaModel()

        desc = PayloadDescriptor(create_callback=capture_availability)
        desc.create_object(MagicMock(), build_params(route=Route("POST", "/x"), request_type=str))

        self.assertFalse(captured["available"])
        self.assertTrue(desc.is_available())


# --- payload_descriptor factory ---

class TestPayloadDescriptorFactory(TestCase):
    def test_returns_payload_descriptor(self):
        desc = payload_descriptor()
        self.assertIsInstance(desc, PayloadDescriptor)

    def test_passes_callbacks(self):
        cr = MagicMock()
        up = MagicMock()
        desc = payload_descriptor(create=cr, update=up)
        self.assertIs(desc._create_callback, cr)
        self.assertIs(desc._update_callback, up)


# --- Descriptors registry: route ---

class TestDescriptorsRouteExact(TestCase):
    def test_add_and_get_route_descriptor(self):
        registry = Descriptors()
        desc = path_descriptor()
        route = Route("GET", "/items")

        registry.add_route_descriptor(route, desc)

        self.assertIs(registry.get_route_descriptor(route), desc)

    def test_get_route_descriptor_returns_none_when_missing(self):
        registry = Descriptors()
        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/missing")))

    def test_different_methods_are_separate(self):
        registry = Descriptors()
        get_desc = path_descriptor()
        post_desc = path_descriptor()

        registry.add_route_descriptor(Route("GET", "/items"), get_desc)
        registry.add_route_descriptor(Route("POST", "/items"), post_desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items")), get_desc)
        self.assertIs(registry.get_route_descriptor(Route("POST", "/items")), post_desc)

    def test_overwrites_existing(self):
        registry = Descriptors()
        desc1 = path_descriptor()
        desc2 = path_descriptor()
        route = Route("GET", "/items")

        registry.add_route_descriptor(route, desc1)
        registry.add_route_descriptor(route, desc2)

        self.assertIs(registry.get_route_descriptor(route), desc2)


# --- Descriptors registry: route regex ---

class TestDescriptorsRouteRegex(TestCase):
    def test_regex_matches_route(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items/.+"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items/123")), desc)

    def test_regex_does_not_match_different_method(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items/.+"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("POST", "/items/123")))

    def test_regex_does_not_match_non_matching_path(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items/.+"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/other/123")))

    def test_regex_fullmatch_not_partial(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/items/extra")))

    def test_exact_match_takes_priority_over_regex(self):
        registry = Descriptors()
        regex_desc = path_descriptor()
        exact_desc = path_descriptor()

        registry.add_route_descriptor(Route("GET", "/items/.+"), regex_desc)
        registry.add_route_descriptor(Route("GET", "/items/123"), exact_desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items/123")), exact_desc)

    def test_regex_with_capture_groups(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items/([0-9]+)/details"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items/42/details")), desc)
        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/items/abc/details")))

    def test_regex_with_path_parameters(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/api/v[0-9]+/items/.+"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/api/v2/items/foo")), desc)

    def test_invalid_regex_is_skipped(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items/[invalid"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/items/123")))

    def test_empty_registry_returns_none(self):
        registry = Descriptors()
        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/anything")))


# --- Descriptors registry: route method regex ---

class TestDescriptorsRouteMethodRegex(TestCase):
    def test_method_regex_matches(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET|POST", "/items"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items")), desc)
        self.assertIs(registry.get_route_descriptor(Route("POST", "/items")), desc)

    def test_method_regex_does_not_match_unlisted_method(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET|POST", "/items"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("DELETE", "/items")))

    def test_method_wildcard_matches_any_method(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route(".*", "/items"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items")), desc)
        self.assertIs(registry.get_route_descriptor(Route("POST", "/items")), desc)
        self.assertIs(registry.get_route_descriptor(Route("DELETE", "/items")), desc)
        self.assertIs(registry.get_route_descriptor(Route("PATCH", "/items")), desc)

    def test_method_regex_fullmatch_not_partial(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET", "/items"), desc)

        self.assertIsNone(registry.get_route_descriptor(Route("GETX", "/items")))

    def test_method_and_path_regex_combined(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_route_descriptor(Route("GET|POST", "/items/.+"), desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items/123")), desc)
        self.assertIs(registry.get_route_descriptor(Route("POST", "/items/abc")), desc)
        self.assertIsNone(registry.get_route_descriptor(Route("DELETE", "/items/123")))
        self.assertIsNone(registry.get_route_descriptor(Route("GET", "/other/123")))

    def test_exact_match_takes_priority_over_method_regex(self):
        registry = Descriptors()
        regex_desc = path_descriptor()
        exact_desc = path_descriptor()

        registry.add_route_descriptor(Route("GET|POST", "/items"), regex_desc)
        registry.add_route_descriptor(Route("GET", "/items"), exact_desc)

        self.assertIs(registry.get_route_descriptor(Route("GET", "/items")), exact_desc)
        self.assertIs(registry.get_route_descriptor(Route("POST", "/items")), regex_desc)


# --- Descriptors registry: interactor ---

class TestDescriptorsInteractor(TestCase):
    def test_add_and_get_exact(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_interactor_descriptor(_BaseInteractor, desc)

        self.assertIs(registry.get_interactor_descriptor(_BaseInteractor), desc)

    def test_returns_none_when_missing(self):
        registry = Descriptors()
        self.assertIsNone(registry.get_interactor_descriptor(_BaseInteractor))

    def test_inheritance_matches_child(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_interactor_descriptor(_BaseInteractor, desc)

        self.assertIs(registry.get_interactor_descriptor(_ChildInteractor), desc)

    def test_inheritance_matches_grandchild(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_interactor_descriptor(_BaseInteractor, desc)

        self.assertIs(registry.get_interactor_descriptor(_GrandchildInteractor), desc)

    def test_inheritance_does_not_match_unrelated(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_interactor_descriptor(_BaseInteractor, desc)

        self.assertIsNone(registry.get_interactor_descriptor(_UnrelatedInteractor))

    def test_exact_match_takes_priority_over_inheritance(self):
        registry = Descriptors()
        base_desc = path_descriptor()
        child_desc = path_descriptor()

        registry.add_interactor_descriptor(_BaseInteractor, base_desc)
        registry.add_interactor_descriptor(_ChildInteractor, child_desc)

        self.assertIs(registry.get_interactor_descriptor(_ChildInteractor), child_desc)
        self.assertIs(registry.get_interactor_descriptor(_GrandchildInteractor), base_desc)

    def test_inheritance_does_not_match_parent(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_interactor_descriptor(_ChildInteractor, desc)

        self.assertIsNone(registry.get_interactor_descriptor(_BaseInteractor))


# --- Descriptors registry: processor ---

class TestDescriptorsProcessor(TestCase):
    def test_add_and_get_exact(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_processor_descriptor(_BaseProcessor, desc)

        self.assertIs(registry.get_processor_descriptor(_BaseProcessor), desc)

    def test_returns_none_when_missing(self):
        registry = Descriptors()
        self.assertIsNone(registry.get_processor_descriptor(_BaseProcessor))

    def test_inheritance_matches_child(self):
        registry = Descriptors()
        desc = path_descriptor()
        registry.add_processor_descriptor(_BaseProcessor, desc)

        self.assertIs(registry.get_processor_descriptor(_ChildProcessor), desc)

    def test_exact_match_takes_priority_over_inheritance(self):
        registry = Descriptors()
        base_desc = path_descriptor()
        child_desc = path_descriptor()

        registry.add_processor_descriptor(_BaseProcessor, base_desc)
        registry.add_processor_descriptor(_ChildProcessor, child_desc)

        self.assertIs(registry.get_processor_descriptor(_ChildProcessor), child_desc)


# --- Descriptors registry: payload ---

class TestDescriptorsPayload(TestCase):
    def test_add_and_get_exact(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(_BaseDto, desc)

        self.assertIs(registry.get_payload_descriptor(_BaseDto), desc)

    def test_returns_none_when_missing(self):
        registry = Descriptors()
        self.assertIsNone(registry.get_payload_descriptor(_BaseDto))

    def test_inheritance_matches_child(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(_BaseDto, desc)

        self.assertIs(registry.get_payload_descriptor(_ChildDto), desc)

    def test_inheritance_does_not_match_unrelated(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(_BaseDto, desc)

        self.assertIsNone(registry.get_payload_descriptor(_UnrelatedDto))

    def test_exact_match_takes_priority_over_inheritance(self):
        registry = Descriptors()
        base_desc = payload_descriptor()
        child_desc = payload_descriptor()

        registry.add_payload_descriptor(_BaseDto, base_desc)
        registry.add_payload_descriptor(_ChildDto, child_desc)

        self.assertIs(registry.get_payload_descriptor(_ChildDto), child_desc)

    def test_inheritance_does_not_match_parent(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(_ChildDto, desc)

        self.assertIsNone(registry.get_payload_descriptor(_BaseDto))


# --- Descriptors registry: payload string/regex matching ---

class TestDescriptorsPayloadStringMatch(TestCase):
    def test_exact_string_matches_class_name(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor("_BaseDto", desc)

        self.assertIs(registry.get_payload_descriptor(_BaseDto), desc)

    def test_regex_matches_class_name(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(r".*Dto", desc)

        self.assertIs(registry.get_payload_descriptor(_BaseDto), desc)
        self.assertIs(registry.get_payload_descriptor(_ChildDto), desc)
        self.assertIs(registry.get_payload_descriptor(_UnrelatedDto), desc)

    def test_regex_no_match(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(r"Foo.*", desc)

        self.assertIsNone(registry.get_payload_descriptor(_BaseDto))

    def test_type_match_takes_priority_over_string(self):
        registry = Descriptors()
        type_desc = payload_descriptor()
        string_desc = payload_descriptor()
        registry.add_payload_descriptor(_BaseDto, type_desc)
        registry.add_payload_descriptor(r".*Dto", string_desc)

        self.assertIs(registry.get_payload_descriptor(_BaseDto), type_desc)

    def test_issubclass_takes_priority_over_string(self):
        registry = Descriptors()
        base_desc = payload_descriptor()
        string_desc = payload_descriptor()
        registry.add_payload_descriptor(_BaseDto, base_desc)
        registry.add_payload_descriptor(r"_ChildDto", string_desc)

        self.assertIs(registry.get_payload_descriptor(_ChildDto), base_desc)

    def test_string_fallback_when_no_type_match(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor(r"_Unrelated.*", desc)

        self.assertIs(registry.get_payload_descriptor(_UnrelatedDto), desc)

    def test_invalid_regex_is_skipped(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor("[invalid", desc)

        self.assertIsNone(registry.get_payload_descriptor(_BaseDto))

    def test_regex_uses_fullmatch(self):
        registry = Descriptors()
        desc = payload_descriptor()
        registry.add_payload_descriptor("Base", desc)

        self.assertIsNone(registry.get_payload_descriptor(_BaseDto))
