"""
Tests for custom component indexing to Qdrant.

Verifies that custom components (e.g., API components not in the Python package)
are properly indexed to Qdrant with symbols, relationships, and searchable embeddings.

The implementation is generic and works with any ModuleWrapper, not just card_framework.
"""

from unittest.mock import MagicMock, patch

import pytest

# Patch the qdrant import at module level for all tests
QDRANT_IMPORT_PATH = "adapters.module_wrapper.qdrant_mixin._get_qdrant_imports"


class TestCustomComponentIndexing:
    """Test custom component indexing functionality."""

    def test_index_custom_components_creates_points(self):
        """Test that index_custom_components creates Qdrant points."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        # Create a mock mixin instance with required attributes
        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {"Section": "§", "Button": "ᵬ"}
        mixin.reverse_symbol_mapping = {"§": "Section", "ᵬ": "Button"}

        # Mock client and embedder
        mock_client = MagicMock()
        mock_embedder = MagicMock()

        # Mock embedding generation - return fresh embedding for each call
        mock_embedding = [0.1] * 384  # Typical embedding dimension
        mock_embedder.embed.side_effect = lambda texts: iter(
            [mock_embedding for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        # Define custom components
        custom_components = {
            "CustomWidgetA": {
                "children": ["CustomWidgetB"],
                "docstring": "Custom widget container",
            },
            "CustomWidgetB": {
                "children": ["NestedChild"],
                "docstring": "Nested custom widget",
            },
        }

        # Call the method
        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MagicMock})
            count = mixin.index_custom_components(custom_components)

        # Verify
        assert count == 2
        mock_client.upsert.assert_called_once()

        # Check that points were created with correct structure
        call_args = mock_client.upsert.call_args
        assert call_args.kwargs["collection_name"] == "test_collection"
        points = call_args.kwargs["points"]
        assert len(points) == 2

    def test_index_custom_components_generates_symbols(self):
        """Test that indexing generates symbols for new components."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {}
        mixin.reverse_symbol_mapping = {}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        custom_components = {
            "TestComponent": {
                "children": [],
                "docstring": "Test component",
            },
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MagicMock})
            mixin.index_custom_components(custom_components)

        # Verify symbol was generated
        assert "TestComponent" in mixin.symbol_mapping
        assert mixin.symbol_mapping["TestComponent"] != ""

    def test_index_custom_components_payload_structure(self):
        """Test that indexed components have correct payload structure."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {"MyWidget": "©"}
        mixin.reverse_symbol_mapping = {"©": "MyWidget"}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        # Capture the PointStruct creation
        captured_payloads = []

        class MockPointStruct:
            def __init__(self, id, vector, payload):
                self.id = id
                self.vector = vector
                self.payload = payload
                captured_payloads.append(payload)

        custom_components = {
            "MyWidget": {
                "children": ["ChildWidget"],
                "docstring": "My custom widget",
                "json_field": "myWidget",
            },
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MockPointStruct})
            mixin.index_custom_components(custom_components)

        # Verify payload structure
        assert len(captured_payloads) == 1
        payload = captured_payloads[0]

        assert payload["name"] == "MyWidget"
        assert payload["type"] == "class"
        assert payload["is_custom_component"] is True
        assert payload["json_field"] == "myWidget"
        assert payload["symbol"] == "©"
        assert "relationships" in payload
        assert payload["relationships"]["child_classes"] == ["ChildWidget"]

    def test_index_custom_components_custom_module_name(self):
        """Test that custom module_name parameter is used."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "default_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {}
        mixin.reverse_symbol_mapping = {}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        captured_payloads = []

        class MockPointStruct:
            def __init__(self, id, vector, payload):
                captured_payloads.append(payload)

        custom_components = {
            "MyWidget": {"children": [], "docstring": "Test"},
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MockPointStruct})
            mixin.index_custom_components(
                custom_components, module_name="custom_api_module"
            )

        # Verify custom module name is used
        assert len(captured_payloads) == 1
        assert captured_payloads[0]["module_path"] == "custom_api_module"
        assert captured_payloads[0]["full_path"] == "custom_api_module.MyWidget"

    def test_index_custom_components_custom_payload_fields(self):
        """Test that custom_payload_fields are added to all components."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {}
        mixin.reverse_symbol_mapping = {}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        captured_payloads = []

        class MockPointStruct:
            def __init__(self, id, vector, payload):
                captured_payloads.append(payload)

        custom_components = {
            "WidgetA": {"children": [], "docstring": "Widget A"},
            "WidgetB": {"children": [], "docstring": "Widget B"},
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MockPointStruct})
            mixin.index_custom_components(
                custom_components,
                custom_payload_fields={
                    "api_version": "v2",
                    "category": "widgets",
                },
            )

        # Verify custom fields are in all payloads
        assert len(captured_payloads) == 2
        for payload in captured_payloads:
            assert payload["api_version"] == "v2"
            assert payload["category"] == "widgets"

    def test_index_custom_components_extra_metadata_fields(self):
        """Test that extra fields in component metadata are preserved."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {}
        mixin.reverse_symbol_mapping = {}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        captured_payloads = []

        class MockPointStruct:
            def __init__(self, id, vector, payload):
                captured_payloads.append(payload)

        custom_components = {
            "MyWidget": {
                "children": [],
                "docstring": "Widget with extra fields",
                "custom_field": "custom_value",
                "priority": 5,
            },
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MockPointStruct})
            mixin.index_custom_components(custom_components)

        # Verify extra fields from metadata are preserved
        assert len(captured_payloads) == 1
        assert captured_payloads[0]["custom_field"] == "custom_value"
        assert captured_payloads[0]["priority"] == 5

    def test_index_custom_components_without_symbol_generation(self):
        """Test indexing without symbol generation."""
        from adapters.module_wrapper.relationships_mixin import RelationshipsMixin

        mixin = RelationshipsMixin()
        mixin.collection_name = "test_collection"
        mixin.module_name = "test_module"
        mixin._cached_relationships = {}
        mixin.symbol_mapping = {}
        mixin.reverse_symbol_mapping = {}

        mock_client = MagicMock()
        mock_embedder = MagicMock()
        mock_embedder.embed.side_effect = lambda texts: iter(
            [[0.1] * 384 for _ in texts]
        )

        mixin.client = mock_client
        mixin.embedder = mock_embedder

        captured_payloads = []

        class MockPointStruct:
            def __init__(self, id, vector, payload):
                captured_payloads.append(payload)

        custom_components = {
            "NoSymbolWidget": {"children": [], "docstring": "No symbol"},
        }

        with patch(QDRANT_IMPORT_PATH) as mock_imports:
            mock_imports.return_value = (None, {"PointStruct": MockPointStruct})
            mixin.index_custom_components(custom_components, generate_symbols=False)

        # Verify no symbol in payload
        assert len(captured_payloads) == 1
        assert "symbol" not in captured_payloads[0]


class TestCustomComponentQdrantIntegration:
    """Integration tests for custom component indexing (requires Qdrant)."""

    @pytest.fixture
    def wrapper(self):
        """Get the singleton wrapper with fresh state."""
        from gchat.card_framework_wrapper import (
            get_card_framework_wrapper,
            reset_wrapper,
        )

        reset_wrapper()
        return get_card_framework_wrapper()

    def test_custom_components_searchable_via_dsl(self, wrapper):
        """Test that custom components are searchable via DSL methods."""
        if not wrapper.client:
            pytest.skip("Qdrant not available")

        # Search for Carousel via search_by_dsl (uses ColBERT)
        results = wrapper.search_by_dsl(
            text="Carousel widget horizontal scrolling",
            limit=5,
            vector_name="components",
            type_filter="class",
        )

        # Should find Carousel or CarouselCard
        names = [r.get("name") for r in results]
        carousel_found = any("Carousel" in name for name in names if name)

        # If no results from DSL search, try regular search
        if not carousel_found:
            # Fall back to text search
            results2 = wrapper.search_by_text(
                field="name",
                query="Carousel",
                limit=3,
            )
            names2 = [r.get("name") for r in results2]
            carousel_found = "Carousel" in names2 or "CarouselCard" in names2

        assert carousel_found, (
            f"Carousel not found via search. DSL search names: {names}"
        )

    def test_custom_components_in_v7_hybrid_search(self, wrapper):
        """Test that custom components appear in V7 hybrid search results."""
        if not wrapper.client:
            pytest.skip("Qdrant not available")

        # Search for carousel-related content
        class_results, patterns, rels = wrapper.search_hybrid(
            description="A carousel with multiple cards",
            limit=10,
            include_classes=True,
        )

        # Check if any carousel-related classes are found
        class_names = [r.get("name") for r in class_results]

        # Custom components should be indexed and searchable
        # May not always return Carousel directly, but should have valid results
        assert len(class_results) > 0 or len(patterns) > 0, (
            "V7 hybrid search returned no results"
        )

        # Log what was found for debugging
        if class_names:
            print(f"Classes found: {class_names[:5]}")

    def test_custom_components_have_symbols(self, wrapper):
        """Test that custom components have generated symbols."""
        # Check symbol mapping includes custom components
        symbols = wrapper.symbol_mapping or {}

        # All 3 truly custom components should have symbols
        custom_names = ["Carousel", "CarouselCard", "NestedWidget"]
        for name in custom_names:
            assert name in symbols, f"{name} not in symbol_mapping"
            assert symbols[name], f"{name} has empty symbol"

    def test_custom_components_in_qdrant(self, wrapper):
        """Test that custom components are stored in Qdrant."""
        if not wrapper.client:
            pytest.skip("Qdrant not available")

        from qdrant_client import models

        # Query for custom components
        results, _ = wrapper.client.scroll(
            collection_name=wrapper.collection_name,
            scroll_filter=models.Filter(
                must=[
                    models.FieldCondition(
                        key="is_custom_component",
                        match=models.MatchValue(value=True),
                    )
                ]
            ),
            limit=10,
            with_payload=True,
        )

        # Should have at least the 3 custom components
        names = [p.payload.get("name") for p in results]
        assert "Carousel" in names, f"Carousel not in Qdrant. Found: {names}"
        assert "CarouselCard" in names, f"CarouselCard not in Qdrant. Found: {names}"
        assert "NestedWidget" in names, f"NestedWidget not in Qdrant. Found: {names}"

    def test_custom_component_relationships_in_qdrant(self, wrapper):
        """Test that custom components have relationships stored in Qdrant."""
        if not wrapper.client:
            pytest.skip("Qdrant not available")

        from qdrant_client import models

        # Query for Carousel specifically
        results, _ = wrapper.client.scroll(
            collection_name=wrapper.collection_name,
            scroll_filter=models.Filter(
                must=[
                    models.FieldCondition(
                        key="name",
                        match=models.MatchValue(value="Carousel"),
                    )
                ]
            ),
            limit=1,
            with_payload=True,
        )

        assert len(results) == 1, "Carousel not found in Qdrant"

        payload = results[0].payload
        assert "relationships" in payload, "Carousel missing relationships"
        assert "CarouselCard" in payload["relationships"]["child_classes"]


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
