"""Tests for the Registry module."""

import threading
from dataclasses import dataclass

import pytest
from pytest import LogCaptureFixture

from pytola_core.patterns.registry import DefaultRegistry, Registry, get_registry, register


class TestRegistry:
    """Test cases for the Registry class."""

    # Basic Registry Creation Tests
    def test_create_registry(self) -> None:
        """Test creating a registry."""
        registry = Registry("test")
        assert registry.name == "test"

    def test_registry_singleton(self) -> None:
        """Test that registries with the same name are singletons."""
        registry1 = Registry("singleton_test")
        registry2 = Registry("singleton_test")
        assert registry1 is registry2

    def test_multiple_registries(self) -> None:
        """Test creating multiple independent registries."""
        registry1 = Registry("test1")
        registry2 = Registry("test2")
        assert registry1 is not registry2
        assert registry1.name == "test1"
        assert registry2.name == "test2"

    # Registration Tests
    def test_register_class(self) -> None:
        """Test registering a class."""

        class TestClass:
            pass

        registry = Registry("register_test")
        registry.register("test_class", TestClass)

        retrieved = registry.get("test_class")
        assert retrieved is TestClass

    def test_register_with_metadata(self) -> None:
        """Test registering a class with metadata."""

        class TestClass:
            pass

        registry = Registry("metadata_test")
        metadata = {"description": "Test class", "version": "1.0"}
        registry.register("test_class", TestClass, metadata=metadata)

        retrieved_metadata = registry.get_metadata("test_class")
        assert retrieved_metadata["description"] == "Test class"
        assert retrieved_metadata["version"] == "1.0"

    def test_register_with_aliases(self) -> None:
        """Test registering a class with aliases."""

        class TestClass:
            pass

        registry = Registry("alias_test")
        registry.register("test_class", TestClass, aliases=["tc", "t"])

        # Get by canonical name
        assert registry.get("test_class") is TestClass
        # Get by alias
        assert registry.get("tc") is TestClass
        assert registry.get("t") is TestClass

    def test_register_overwrite_warning(self, caplog: LogCaptureFixture) -> None:
        """Test that overwriting a registration produces a warning."""

        class TestClass1:
            pass

        class TestClass2:
            pass

        registry = Registry("overwrite_test")
        registry.register("test_class", TestClass1)
        registry.register("test_class", TestClass2)

        assert "already registered" in caplog.text
        assert registry.get("test_class") is TestClass2

    def test_alias_overwrite_warning(self, caplog: LogCaptureFixture) -> None:
        """Test that overwriting an alias produces a warning."""

        class TestClass1:
            pass

        class TestClass2:
            pass

        registry = Registry("alias_overwrite_test")
        registry.register("class1", TestClass1, aliases=["a"])
        registry.register("class2", TestClass2, aliases=["a"])

        assert "already registered" in caplog.text
        assert registry.get("a") is TestClass2

    # Input Validation Tests
    def test_register_empty_name(self) -> None:
        """Test that empty registration name raises ValueError."""
        registry = Registry("validation_test")

        with pytest.raises(ValueError, match="cannot be empty"):
            registry.register("", object)

    def test_register_long_name(self) -> None:
        """Test that long registration name raises ValueError."""
        registry = Registry("validation_test")
        long_name = "x" * 100

        with pytest.raises(ValueError, match="exceeds maximum length"):
            registry.register(long_name, object)

    def test_register_empty_alias(self) -> None:
        """Test that empty alias raises ValueError."""
        registry = Registry("validation_test")

        with pytest.raises(ValueError, match="cannot be empty string"):
            registry.register("valid", object, aliases=[""])

    def test_register_long_alias(self) -> None:
        """Test that long alias raises ValueError."""
        registry = Registry("validation_test")
        long_alias = "y" * 100

        with pytest.raises(ValueError, match="exceeds maximum length"):
            registry.register("valid", object, aliases=[long_alias])

    # Unregister Tests
    def test_unregister_class(self) -> None:
        """Test unregistering a class."""

        class TestClass:
            pass

        registry = Registry("unregister_test")
        registry.register("test_class", TestClass)

        # Verify it's registered
        assert registry.get("test_class") is TestClass

        # Unregister it
        registry.unregister("test_class")

        # Verify it's gone
        assert registry.get("test_class") is None

    def test_unregister_removes_aliases(self) -> None:
        """Test that unregistering removes associated aliases."""

        class TestClass:
            pass

        registry = Registry("unregister_alias_test")
        registry.register("test_class", TestClass, aliases=["tc", "t"])

        # Verify aliases work
        assert registry.get("tc") is TestClass

        # Unregister
        registry.unregister("test_class")

        # Verify aliases are removed
        assert registry.get("tc") is None
        assert registry.get("t") is None

    def test_unregister_non_existent_warning(self, caplog: LogCaptureFixture) -> None:
        """Test that unregistering non-existent item produces a warning."""
        registry = Registry("nonexistent_test")
        registry.unregister("does_not_exist")

        assert "non-existent" in caplog.text

    def test_unregister_preserves_other_items(self) -> None:
        """Test that unregistering one item doesn't affect others."""

        class TestClass1:
            pass

        class TestClass2:
            pass

        registry = Registry("preserve_test")
        registry.register("class1", TestClass1)
        registry.register("class2", TestClass2)

        registry.unregister("class1")

        # class2 should still be registered
        assert registry.get("class2") is TestClass2
        assert registry.get("class1") is None

    # Get Methods Tests
    def test_get_non_existent(self) -> None:
        """Test getting a non-existent item returns None."""
        registry = Registry("get_test")
        result = registry.get("does_not_exist")
        assert result is None

    def test_get_instance(self) -> None:
        """Test getting an instance of a registered class."""

        @dataclass
        class TestClass:
            value: int = 42

        registry = Registry("instance_test")
        registry.register("test_class", TestClass)

        instance = registry.get_instance("test_class")
        assert isinstance(instance, TestClass)
        assert instance.value == 42

    def test_get_instance_with_args(self) -> None:
        """Test getting an instance with constructor arguments."""

        @dataclass
        class TestClass:
            value: int

        registry = Registry("instance_args_test")
        registry.register("test_class", TestClass)

        instance = registry.get_instance("test_class", 100)
        assert isinstance(instance, TestClass)
        assert instance.value == 100

    def test_get_instance_non_existent(self) -> None:
        """Test getting an instance of non-existent class returns None."""
        registry = Registry("instance_none_test")
        result = registry.get_instance("does_not_exist")
        assert result is None

    # List Methods Tests
    def test_get_classes(self) -> None:
        """Test getting all registered classes."""

        class Class1:
            pass

        class Class2:
            pass

        registry = Registry("list_test")
        registry.register("class1", Class1)
        registry.register("class2", Class2)

        classes = registry.get_classes()
        assert len(classes) == 2
        assert Class1 in classes
        assert Class2 in classes

    def test_list_items(self) -> None:
        """Test listing all registered item names."""

        class Class1:
            pass

        class Class2:
            pass

        registry = Registry("list_names_test")
        registry.register("class1", Class1)
        registry.register("class2", Class2)

        names = registry.list_items()
        assert len(names) == 2
        assert "class1" in names
        assert "class2" in names

    def test_list_all(self) -> None:
        """Test listing all items with metadata."""

        class Class1:
            pass

        registry = Registry("list_all_test")
        metadata = {"description": "Test"}
        registry.register("class1", Class1, aliases=["c1"], metadata=metadata)

        all_items = registry.list_all()
        assert len(all_items) == 1
        assert "class1" in all_items
        assert all_items["class1"]["description"] == "Test"
        assert all_items["class1"]["class"] is Class1
        assert "c1" in all_items["class1"]["aliases"]

    def test_get_metadata_default(self) -> None:
        """Test getting metadata returns empty dict for non-existent."""
        registry = Registry("metadata_default_test")
        metadata = registry.get_metadata("does_not_exist")
        assert metadata == {}

    # Special Methods Tests
    def test_len(self) -> None:
        """Test __len__ method."""
        registry = Registry("len_test")
        assert len(registry) == 0

        registry.register("class1", object)
        assert len(registry) == 1

        registry.register("class2", object)
        assert len(registry) == 2

    def test_contains_by_name(self) -> None:
        """Test __contains__ method with canonical name."""
        registry = Registry("contains_test")
        registry.register("test_class", object)

        assert "test_class" in registry
        assert "other_class" not in registry

    def test_contains_by_alias(self) -> None:
        """Test __contains__ method with alias."""
        registry = Registry("contains_alias_test")
        registry.register("test_class", object, aliases=["tc"])

        assert "tc" in registry
        assert "test_class" in registry

    # Clear Tests
    def test_clear(self) -> None:
        """Test clearing a registry."""

        class TestClass:
            pass

        registry = Registry("clear_test")
        registry.register("test_class", TestClass, aliases=["tc"])

        registry.clear()

        assert len(registry) == 0
        assert registry.get("test_class") is None
        assert registry.get("tc") is None

    def test_clear_all(self) -> None:
        """Test clearing all registries."""
        registry1 = Registry("clear_all_1")
        registry2 = Registry("clear_all_2")

        registry1.register("class1", object)
        registry2.register("class2", object)

        Registry.clear_all()

        assert len(registry1) == 0
        assert len(registry2) == 0

    # Thread Safety Tests
    def test_thread_safe_creation(self) -> None:
        """Test thread-safe registry creation."""
        registries = []
        lock = threading.Lock()

        def create_registry(name: str) -> None:
            reg = Registry(f"thread_{name}")
            with lock:
                registries.append(reg)

        threads = [threading.Thread(target=create_registry, args=(str(i),)) for i in range(5)]

        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()

        assert len(registries) == 5
        # All should be unique
        names = [reg.name for reg in registries]
        assert len(set(names)) == 5

    def test_thread_safe_initialization(self) -> None:
        """Test thread-safe registry initialization."""
        Registry("thread_init_test")

        def access_registry() -> None:
            # Multiple threads accessing the same registry
            _ = Registry("thread_init_test")

        threads = [threading.Thread(target=access_registry) for _ in range(5)]

        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()

        # Should not raise any exceptions

    # Class Method Tests
    def test_get_registry_class_method(self) -> None:
        """Test get_registry class method."""
        registry = Registry.get_registry("class_method_test")
        assert registry.name == "class_method_test"

    def test_list_registries(self) -> None:
        """Test list_registries class method."""
        Registry.clear_all()

        Registry("registry1")
        Registry("registry2")

        registry_names = Registry.list_registries()
        assert "registry1" in registry_names
        assert "registry2" in registry_names


class TestDefaultRegistry:
    """Test cases for the DefaultRegistry class."""

    def test_default_registry_singleton(self) -> None:
        """Test that DefaultRegistry is a singleton."""
        default1 = DefaultRegistry()
        default2 = DefaultRegistry()
        assert default1 is default2

    def test_default_registry_get_default_registry(self) -> None:
        """Test get_default_registry class method."""
        default1 = DefaultRegistry.get_default_registry()
        default2 = DefaultRegistry.get_default_registry()
        assert default1 is default2

    def test_default_registry_name(self) -> None:
        """Test that default registry has name 'default'."""
        default = DefaultRegistry()
        assert default.name == "default"

    def test_default_registry_backward_compatibility(self) -> None:
        """Test backward compatibility with regular Registry."""
        default = DefaultRegistry()
        also_default = Registry("default")
        assert default is also_default


class TestRegisterDecorator:
    """Test cases for the register decorator."""

    def test_decorator_new_style(self) -> None:
        """Test new style decorator with registry name."""

        @register("decorator_test", name="my_class")
        class MyClass:
            pass

        registry = Registry("decorator_test")
        assert registry.get("my_class") is MyClass

    def test_decorator_default_name(self) -> None:
        """Test decorator using class name as default."""

        @register("default_name_test")
        class AutoNamedClass:
            pass

        registry = Registry("default_name_test")
        assert registry.get("autonamedclass") is AutoNamedClass

    def test_decorator_with_aliases(self) -> None:
        """Test decorator with aliases."""

        @register("alias_dec_test", name="my_class", aliases=["mc", "m"])
        class MyClass:
            pass

        registry = Registry("alias_dec_test")
        assert registry.get("my_class") is MyClass
        assert registry.get("mc") is MyClass
        assert registry.get("m") is MyClass

    def test_decorator_with_metadata(self) -> None:
        """Test decorator with metadata."""

        @register("metadata_dec_test", name="my_class", description="A test class")
        class MyClass:
            pass

        registry = Registry("metadata_dec_test")
        metadata = registry.get_metadata("my_class")
        assert metadata["description"] == "A test class"
        assert metadata["class_name"] == "MyClass"

    def test_decorator_legacy_style(self) -> None:
        """Test legacy style decorator without registry name."""

        @register(name="legacy_class", aliases=["lc"])
        class LegacyClass:
            pass

        registry = Registry("default")
        assert registry.get("legacy_class") is LegacyClass
        assert registry.get("lc") is LegacyClass

    def test_decorator_class_metadata_attribute(self) -> None:
        """Test that decorator stores metadata on class."""

        @register("attr_test", name="my_class", description="Test")
        class MyClass:
            pass

        assert hasattr(MyClass, "_registry_metadata")
        assert MyClass._registry_metadata["description"] == "Test"
        assert MyClass._registry_name == "attr_test"


class TestGetRegistry:
    """Test cases for the get_registry function."""

    def test_get_registry_function(self) -> None:
        """Test get_registry convenience function."""
        registry = get_registry("function_test")
        assert registry.name == "function_test"

        # Should be the same as calling Registry directly
        same_registry = Registry("function_test")
        assert registry is same_registry


class TestMetadataFormat:
    """Test cases for metadata format."""

    def test_metadata_structure(self) -> None:
        """Test registry metadata structure."""
        metadata = {
            "name": "test",
            "class_name": "TestClass",
            "description": "A test class",
            "help_text": "Help text here",
            "aliases": ["t", "tc"],
        }

        assert metadata["name"] == "test"
        assert metadata["class_name"] == "TestClass"
        assert metadata["description"] == "A test class"
        assert metadata["help_text"] == "Help text here"
        assert metadata["aliases"] == ["t", "tc"]

    def test_metadata_optional_fields(self) -> None:
        """Test that metadata fields are optional."""
        # Should be able to create partial metadata
        metadata = {"name": "test", "class_name": "TestClass"}

        assert metadata["name"] == "test"
        assert metadata["class_name"] == "TestClass"


class TestEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_max_length_name(self) -> None:
        """Test registration with maximum length name."""
        registry = Registry("max_length_test")
        max_length_name = "x" * 64  # Exactly at the limit

        # Should not raise
        registry.register(max_length_name, object)
        assert registry.get(max_length_name) is object

    def test_max_length_alias(self) -> None:
        """Test registration with maximum length alias."""
        registry = Registry("max_alias_test")
        max_length_alias = "y" * 64  # Exactly at the limit

        # Should not raise
        registry.register("valid", object, aliases=[max_length_alias])
        assert registry.get(max_length_alias) is object

    def test_special_characters_in_name(self) -> None:
        """Test registration with special characters in name."""
        registry = Registry("special_chars_test")
        special_name = "test-class_123.test/class"

        # Should accept special characters
        registry.register(special_name, object)
        assert registry.get(special_name) is object

    def test_unicode_in_name(self) -> None:
        """Test registration with unicode characters."""
        registry = Registry("unicode_test")
        unicode_name = "测试类_テスト"

        registry.register(unicode_name, object)
        assert registry.get(unicode_name) is object

    def test_case_sensitivity(self) -> None:
        """Test that names are case-sensitive."""
        registry = Registry("case_test")

        class Class1:
            pass

        class Class2:
            pass

        registry.register("TestClass", Class1)
        registry.register("testclass", Class2)

        # Should be different registrations
        assert registry.get("TestClass") is Class1
        assert registry.get("testclass") is Class2
        assert len(registry) == 2

    def test_get_instance_with_kwargs(self) -> None:
        """Test getting instance with keyword arguments."""

        class TestClass:
            def __init__(self, value: int = 10, multiplier: int = 1) -> None:
                self.result = value * multiplier

        registry = Registry("kwargs_test")
        registry.register("test", TestClass)

        instance = registry.get_instance("test", value=5, multiplier=3)
        assert instance.result == 15
