"""
Tests for LLM Registry.

Tests cover:
- Registration of custom LLM providers
- Deregistration
- Getting registered providers
- Listing all registered providers
- Is-registered checks
- Clearing the registry
- Thread safety with concurrent operations
- Edge cases (case sensitivity, overwrite, special chars)
- Module-level public API functions
- Singleton behavior
"""

import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from unittest.mock import Mock

import pytest


class TestLLMRegistryRegistration:
    """Tests for register method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_register_callable_factory(self):
        """Register a callable factory function."""
        registry = self._make_registry()

        def factory():
            return Mock()

        registry.register("my_llm", factory)

        assert registry.get("my_llm") is factory

    def test_register_mock_model_instance(self):
        """Register a mock model instance."""
        registry = self._make_registry()
        model = Mock()

        registry.register("test_model", model)

        assert registry.get("test_model") is model

    def test_register_multiple_providers(self):
        """Register multiple distinct providers."""
        registry = self._make_registry()
        model1 = Mock()
        model2 = Mock()

        registry.register("model1", model1)
        registry.register("model2", model2)

        assert registry.get("model1") is model1
        assert registry.get("model2") is model2

    def test_register_overwrites_existing(self):
        """Registering with existing name overwrites the previous entry."""
        registry = self._make_registry()
        model1 = Mock()
        model2 = Mock()

        registry.register("same_name", model1)
        registry.register("same_name", model2)

        assert registry.get("same_name") is model2


class TestLLMRegistryDeregistration:
    """Tests for unregister method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_unregister_existing(self):
        """Unregister an existing provider."""
        registry = self._make_registry()
        registry.register("to_remove", Mock())

        registry.unregister("to_remove")

        assert registry.get("to_remove") is None

    def test_unregister_nonexistent_no_error(self):
        """Unregistering a non-existent provider does not raise."""
        registry = self._make_registry()

        # Should not raise
        registry.unregister("nonexistent")

    def test_unregister_does_not_affect_others(self):
        """Unregistering one provider does not affect others."""
        registry = self._make_registry()
        model_keep = Mock()
        registry.register("keep", model_keep)
        registry.register("remove", Mock())

        registry.unregister("remove")

        assert registry.get("keep") is model_keep

    def test_unregister_case_insensitive(self):
        """Unregister works case-insensitively."""
        registry = self._make_registry()
        registry.register("MyModel", Mock())

        registry.unregister("mymodel")

        assert registry.get("MyModel") is None


class TestLLMRegistryGet:
    """Tests for get method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_get_registered_provider(self):
        """Get a registered provider."""
        registry = self._make_registry()
        model = Mock()
        registry.register("test", model)

        assert registry.get("test") is model

    def test_get_nonexistent_returns_none(self):
        """Get non-existent provider returns None."""
        registry = self._make_registry()

        assert registry.get("nonexistent") is None

    def test_get_case_insensitive(self):
        """Get works case-insensitively."""
        registry = self._make_registry()
        model = Mock()
        registry.register("MyModel", model)

        assert registry.get("mymodel") is model
        assert registry.get("MYMODEL") is model
        assert registry.get("MyModel") is model

    def test_get_after_unregister(self):
        """Get returns None after provider is unregistered."""
        registry = self._make_registry()
        registry.register("temp", Mock())
        registry.unregister("temp")

        assert registry.get("temp") is None


class TestLLMRegistryListRegistered:
    """Tests for list_registered method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_list_with_providers(self):
        """List returns all registered names."""
        registry = self._make_registry()
        registry.register("alpha", Mock())
        registry.register("beta", Mock())

        names = registry.list_registered()

        assert sorted(names) == ["alpha", "beta"]

    def test_list_empty_registry(self):
        """List returns empty for empty registry."""
        registry = self._make_registry()

        assert registry.list_registered() == []

    def test_list_returns_normalized_names(self):
        """List returns lowercase normalized names."""
        registry = self._make_registry()
        registry.register("MyModel", Mock())
        registry.register("ANOTHER", Mock())

        names = registry.list_registered()

        assert "mymodel" in names
        assert "another" in names

    def test_list_returns_copy(self):
        """Modifying returned list does not affect registry."""
        registry = self._make_registry()
        registry.register("test", Mock())

        names = registry.list_registered()
        names.append("fake")

        assert "fake" not in registry.list_registered()


class TestLLMRegistryIsRegistered:
    """Tests for is_registered method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_is_registered_true(self):
        """Returns True for registered provider."""
        registry = self._make_registry()
        registry.register("test", Mock())

        assert registry.is_registered("test") is True

    def test_is_registered_false(self):
        """Returns False for unregistered provider."""
        registry = self._make_registry()

        assert registry.is_registered("nonexistent") is False

    def test_is_registered_case_insensitive(self):
        """Is_registered works case-insensitively."""
        registry = self._make_registry()
        registry.register("MyModel", Mock())

        assert registry.is_registered("mymodel") is True
        assert registry.is_registered("MYMODEL") is True

    def test_is_registered_after_clear(self):
        """Returns False after registry is cleared."""
        registry = self._make_registry()
        registry.register("test", Mock())
        registry.clear()

        assert registry.is_registered("test") is False


class TestLLMRegistryClear:
    """Tests for clear method."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_clear_removes_all(self):
        """Clear removes all registered providers."""
        registry = self._make_registry()
        registry.register("a", Mock())
        registry.register("b", Mock())

        registry.clear()

        assert registry.list_registered() == []
        assert registry.get("a") is None
        assert registry.get("b") is None

    def test_clear_empty_no_error(self):
        """Clear on empty registry does not raise."""
        registry = self._make_registry()

        registry.clear()  # Should not raise

    def test_register_after_clear(self):
        """Can register after clear."""
        registry = self._make_registry()
        registry.register("old", Mock())
        registry.clear()

        model = Mock()
        registry.register("new", model)

        assert registry.get("new") is model


class TestLLMRegistryThreadSafety:
    """Tests for thread safety."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_concurrent_registration(self):
        """Concurrent registrations from multiple threads don't corrupt state."""
        registry = self._make_registry()
        num_threads = 20
        barrier = threading.Barrier(num_threads)

        def register_model(idx):
            barrier.wait()
            model = Mock()
            registry.register(f"model_{idx}", model)
            return idx

        with ThreadPoolExecutor(max_workers=num_threads) as executor:
            futures = [
                executor.submit(register_model, i) for i in range(num_threads)
            ]
            for f in as_completed(futures):
                f.result()  # Ensure no exceptions

        # All models should be registered
        assert len(registry.list_registered()) == num_threads

    def test_concurrent_read_write(self):
        """Concurrent reads and writes don't cause errors."""
        registry = self._make_registry()
        registry.register("base", Mock())
        errors = []

        def writer(idx):
            try:
                registry.register(f"write_{idx}", Mock())
            except Exception as e:
                errors.append(e)

        def reader():
            try:
                registry.get("base")
                registry.list_registered()
                registry.is_registered("base")
            except Exception as e:
                errors.append(e)

        threads = []
        for i in range(10):
            threads.append(threading.Thread(target=writer, args=(i,)))
            threads.append(threading.Thread(target=reader))

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

        assert errors == []

    def test_concurrent_register_unregister(self):
        """Concurrent register and unregister don't corrupt state."""
        registry = self._make_registry()
        errors = []

        def register_and_unregister(idx):
            try:
                name = f"temp_{idx}"
                registry.register(name, Mock())
                registry.unregister(name)
            except Exception as e:
                errors.append(e)

        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [
                executor.submit(register_and_unregister, i) for i in range(50)
            ]
            for f in as_completed(futures):
                f.result()

        assert errors == []


class TestLLMRegistryEdgeCases:
    """Tests for edge cases."""

    def _make_registry(self):
        from local_deep_research.llm.llm_registry import LLMRegistry

        return LLMRegistry()

    def test_register_none_name_raises(self):
        """Register with None name raises AttributeError."""
        registry = self._make_registry()

        with pytest.raises(AttributeError):
            registry.register(None, Mock())

    def test_case_normalization(self):
        """Names are normalized to lowercase."""
        registry = self._make_registry()
        model = Mock()

        registry.register("MyMODEL_Test", model)

        assert registry.get("mymodel_test") is model
        assert "mymodel_test" in registry.list_registered()

    def test_case_insensitive_overwrite(self):
        """Registering same name with different case overwrites."""
        registry = self._make_registry()
        model1 = Mock()
        model2 = Mock()

        registry.register("MyModel", model1)
        registry.register("mymodel", model2)

        assert registry.get("mymodel") is model2
        assert len(registry.list_registered()) == 1

    def test_empty_string_name(self):
        """Empty string name can be registered."""
        registry = self._make_registry()
        model = Mock()

        registry.register("", model)

        assert registry.get("") is model

    def test_special_characters_in_name(self):
        """Names with special characters work correctly."""
        registry = self._make_registry()
        model = Mock()

        registry.register("my-model/v1.0", model)

        assert registry.get("my-model/v1.0") is model

    def test_unicode_name(self):
        """Unicode names work correctly."""
        registry = self._make_registry()
        model = Mock()

        registry.register("模型", model)

        assert registry.get("模型") is model


class TestModuleLevelFunctions:
    """Tests for module-level public API functions."""

    def setup_method(self):
        """Clear global registry before each test."""
        from local_deep_research.llm.llm_registry import clear_llm_registry

        clear_llm_registry()

    def teardown_method(self):
        """Clear global registry after each test."""
        from local_deep_research.llm.llm_registry import clear_llm_registry

        clear_llm_registry()

    def test_register_llm(self):
        """register_llm adds to global registry."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            get_llm_from_registry,
        )

        model = Mock()
        register_llm("test", model)

        assert get_llm_from_registry("test") is model

    def test_unregister_llm(self):
        """unregister_llm removes from global registry."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            unregister_llm,
            get_llm_from_registry,
        )

        register_llm("test", Mock())
        unregister_llm("test")

        assert get_llm_from_registry("test") is None

    def test_is_llm_registered(self):
        """is_llm_registered checks global registry."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            is_llm_registered,
        )

        register_llm("test", Mock())

        assert is_llm_registered("test") is True
        assert is_llm_registered("nonexistent") is False

    def test_list_registered_llms(self):
        """list_registered_llms returns global registry contents."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            list_registered_llms,
        )

        register_llm("alpha", Mock())
        register_llm("beta", Mock())

        names = list_registered_llms()
        assert sorted(names) == ["alpha", "beta"]

    def test_clear_llm_registry(self):
        """clear_llm_registry clears the global registry."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            clear_llm_registry,
            list_registered_llms,
        )

        register_llm("test", Mock())
        clear_llm_registry()

        assert list_registered_llms() == []

    def test_case_insensitive_via_module_functions(self):
        """Module-level functions are case-insensitive."""
        from local_deep_research.llm.llm_registry import (
            register_llm,
            get_llm_from_registry,
            is_llm_registered,
        )

        model = Mock()
        register_llm("MyModel", model)

        assert get_llm_from_registry("mymodel") is model
        assert is_llm_registered("MYMODEL") is True


class TestLLMRegistrySingleton:
    """Tests for singleton behavior of global registry."""

    def test_global_registry_is_same_instance(self):
        """Multiple imports of _llm_registry return same instance."""
        from local_deep_research.llm.llm_registry import _llm_registry as reg1
        from local_deep_research.llm.llm_registry import _llm_registry as reg2

        assert reg1 is reg2

    def test_global_registry_is_llm_registry_type(self):
        """Global registry is an instance of LLMRegistry."""
        from local_deep_research.llm.llm_registry import (
            _llm_registry,
            LLMRegistry,
        )

        assert isinstance(_llm_registry, LLMRegistry)
