"""
Tests for conversation and message search functionality.

This test suite covers:
- Conversation vector search
- ChatMessage vector search
- Permission filtering
- GraphQL query integration
"""

import pytest
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.test import TestCase, override_settings
from graphene.test import Client
from graphql_relay import to_global_id

from config.graphql.schema import schema
from opencontractserver.annotations.models import Embedding
from opencontractserver.conversations.models import ChatMessage, Conversation
from opencontractserver.corpuses.models import Corpus
from opencontractserver.documents.models import Document
from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
    CoreChatMessageVectorStore,
    CoreConversationVectorStore,
    VectorSearchQuery,
)
from opencontractserver.utils.permissioning import (
    PermissionTypes,
    set_permissions_for_obj_to_user,
)

User = get_user_model()


class TestContext:
    def __init__(self, user):
        self.user = user


class ConversationVectorSearchTest(TestCase):
    """Test conversation vector search functionality."""

    def setUp(self):
        """Set up test data."""
        # Create users
        self.user = User.objects.create_user(
            username="search_testuser", password="testpassword"
        )
        self.other_user = User.objects.create_user(
            username="other_user", password="testpassword"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Search Test Corpus", creator=self.user
        )

        # Create document
        pdf_file = ContentFile(b"%PDF-1.4 test pdf content", name="test_search.pdf")
        self.doc = Document.objects.create(
            creator=self.user,
            title="Search Test Document",
            description="Test document for search",
            custom_meta={},
            pdf_file=pdf_file,
            backend_lock=True,
        )
        self.corpus.add_document(document=self.doc, user=self.user)

        # Create conversations with different content
        self.conv1 = Conversation.objects.create(
            title="Machine Learning in Legal Analytics",
            description="Discussion about using ML for contract analysis",
            chat_with_corpus=self.corpus,
            creator=self.user,
            conversation_type="thread",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conv1,
            permissions=[PermissionTypes.ALL],
        )

        self.conv2 = Conversation.objects.create(
            title="Natural Language Processing Techniques",
            description="Exploring NLP methods for document understanding",
            chat_with_corpus=self.corpus,
            creator=self.user,
            conversation_type="thread",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conv2,
            permissions=[PermissionTypes.ALL],
        )

        self.conv3 = Conversation.objects.create(
            title="Agent-based Document Chat",
            description="Chat with document using AI agents",
            chat_with_document=self.doc,
            creator=self.user,
            conversation_type="chat",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conv3,
            permissions=[PermissionTypes.ALL],
        )

        # Create a private CHAT conversation owned by other_user
        # Note: We use CHAT type (not THREAD) because THREAD type conversations
        # inherit visibility from their corpus - anyone who can see the corpus
        # can see its threads. CHAT type uses restrictive permissions (only
        # creator, explicit guardian permissions, or public).
        self.other_conv = Conversation.objects.create(
            title="Private Discussion",
            description="This should not be visible to main user",
            chat_with_corpus=self.corpus,
            creator=self.other_user,
            conversation_type="chat",
            is_public=False,
        )

        # Create embeddings for conversations (using mock embeddings)
        # In real usage, these would be generated by an actual embedder
        self.create_mock_embedding(self.conv1, [0.1] * 384)
        self.create_mock_embedding(self.conv2, [0.2] * 384)
        self.create_mock_embedding(self.conv3, [0.3] * 384)
        self.create_mock_embedding(self.other_conv, [0.4] * 384)

    def create_mock_embedding(self, conversation, vector):
        """Create a mock embedding for a conversation."""
        Embedding.objects.create(
            conversation=conversation,
            embedder_path="test/embedder",
            vector_384=vector,
            creator=conversation.creator,
        )

    def test_conversation_has_embedding_mixin(self):
        """Test that Conversation model has HasEmbeddingMixin functionality."""
        # Test get_embedding_reference_kwargs
        kwargs = self.conv1.get_embedding_reference_kwargs()
        self.assertEqual(kwargs, {"conversation_id": self.conv1.pk})

        # Test add_embedding
        test_vector = [0.5] * 384
        embedding = self.conv1.add_embedding("test/new-embedder", test_vector)
        self.assertIsNotNone(embedding)
        assert embedding is not None
        self.assertEqual(embedding.conversation_id, self.conv1.pk)

        # Test get_embedding
        retrieved_vector = self.conv1.get_embedding("test/new-embedder", 384)
        assert retrieved_vector is not None
        self.assertEqual(len(retrieved_vector), 384)

    def test_conversation_queryset_vector_search(self):
        """Test ConversationQuerySet vector search capabilities."""
        # Create a query vector
        query_vector = [0.15] * 384  # Should be close to conv1's [0.1]*384

        # Perform search
        results = Conversation.objects.search_by_embedding(
            query_vector=query_vector,
            embedder_path="test/embedder",
            top_k=2,
        )

        # Should return results
        self.assertGreater(len(results), 0)

        # Results should have similarity_score annotation
        for conv in results:
            self.assertTrue(hasattr(conv, "similarity_score"))
            self.assertIsNotNone(conv.similarity_score)

    def test_core_conversation_vector_store_permissions(self):
        """Test that CoreConversationVectorStore respects user permissions."""
        # Create vector store for main user with corpus filter
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        # Create search query
        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        # Perform search
        results = store.search(query)

        # Should only return conversations visible to user AND linked to corpus
        conversation_ids = {result.conversation.id for result in results}

        # Should include user's corpus-linked conversations
        self.assertIn(self.conv1.id, conversation_ids)
        self.assertIn(self.conv2.id, conversation_ids)

        # Should NOT include document-linked conversation (not part of corpus filter)
        self.assertNotIn(self.conv3.id, conversation_ids)

        # Should NOT include other user's private conversation
        self.assertNotIn(self.other_conv.id, conversation_ids)

        # Test without corpus filter to verify all user's conversations are visible
        store_no_filter = CoreConversationVectorStore(
            user_id=self.user.id,
            embedder_path="test/embedder",
        )

        results_all = store_no_filter.search(query)
        all_conversation_ids = {result.conversation.id for result in results_all}

        # Now all user's conversations should be included
        self.assertIn(self.conv1.id, all_conversation_ids)
        self.assertIn(self.conv2.id, all_conversation_ids)
        self.assertIn(self.conv3.id, all_conversation_ids)
        self.assertNotIn(self.other_conv.id, all_conversation_ids)

    def test_core_conversation_vector_store_corpus_filter(self):
        """Test filtering by corpus."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should include corpus conversations
        self.assertIn(self.conv1.id, conversation_ids)
        self.assertIn(self.conv2.id, conversation_ids)

    def test_core_conversation_vector_store_conversation_type_filter(self):
        """Test filtering by conversation type."""
        # Filter for threads only
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            conversation_type="thread",
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should include thread conversations
        self.assertIn(self.conv1.id, conversation_ids)
        self.assertIn(self.conv2.id, conversation_ids)

        # Should NOT include chat conversations
        self.assertNotIn(self.conv3.id, conversation_ids)

    def test_soft_delete_exclusion(self):
        """Test that soft-deleted conversations are excluded."""
        # Soft delete a conversation
        from django.utils import timezone

        self.conv2.deleted_at = timezone.now()
        self.conv2.save()

        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should NOT include deleted conversation
        self.assertNotIn(self.conv2.id, conversation_ids)

    def test_search_with_query_text(self):
        """Test search using query_text (generates embedding)."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_text="test conversation search",
            similarity_top_k=10,
        )

        # Will fail without real embedder, but tests the code path
        try:
            results = store.search(query)
            self.assertIsInstance(results, list)
        except (ValueError, AttributeError, TypeError) as e:
            # Expected without real embedder service
            error_msg = str(e).lower()
            self.assertTrue(
                "len(" in error_msg or "embedder" in error_msg or "vector" in error_msg
            )

    def test_search_missing_query_raises_error(self):
        """Test that search raises ValueError when neither text nor embedding provided."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            similarity_top_k=10,
        )

        with self.assertRaises(ValueError) as ctx:
            store.search(query)

        self.assertIn("Either query_text or query_embedding", str(ctx.exception))


class MessageVectorSearchTest(TestCase):
    """Test message vector search functionality."""

    def setUp(self):
        """Set up test data."""
        # Create user
        self.user = User.objects.create_user(
            username="msg_search_user", password="testpassword"
        )
        self.other_user = User.objects.create_user(
            username="other_msg_user", password="testpassword"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Message Search Corpus", creator=self.user
        )

        # Create conversation
        self.conversation = Conversation.objects.create(
            title="Test Thread",
            chat_with_corpus=self.corpus,
            creator=self.user,
            conversation_type="thread",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conversation,
            permissions=[PermissionTypes.ALL],
        )

        # Create messages
        self.msg1 = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="How do I analyze legal contracts using machine learning?",
        )

        self.msg2 = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="LLM",
            content="You can use NLP techniques like named entity recognition and document classification.",
        )

        self.msg3 = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="What about extracting specific clauses?",
        )

        # Create embeddings for messages
        self.create_mock_message_embedding(self.msg1, [0.1] * 384)
        self.create_mock_message_embedding(self.msg2, [0.2] * 384)
        self.create_mock_message_embedding(self.msg3, [0.3] * 384)

    def create_mock_message_embedding(self, message, vector):
        """Create a mock embedding for a message."""
        Embedding.objects.create(
            message=message,
            embedder_path="test/embedder",
            vector_384=vector,
            creator=message.creator,
        )

    def test_message_has_embedding_mixin(self):
        """Test that ChatMessage model has HasEmbeddingMixin functionality."""
        # Test get_embedding_reference_kwargs
        kwargs = self.msg1.get_embedding_reference_kwargs()
        self.assertEqual(kwargs, {"message_id": self.msg1.pk})

        # Test add_embedding
        test_vector = [0.5] * 384
        embedding = self.msg1.add_embedding("test/new-embedder", test_vector)
        self.assertIsNotNone(embedding)
        assert embedding is not None
        self.assertEqual(embedding.message_id, self.msg1.pk)

        # Test get_embedding
        retrieved_vector = self.msg1.get_embedding("test/new-embedder", 384)
        assert retrieved_vector is not None
        self.assertEqual(len(retrieved_vector), 384)

    def test_message_queryset_vector_search(self):
        """Test ChatMessageQuerySet vector search capabilities."""
        query_vector = [0.15] * 384

        results = ChatMessage.objects.search_by_embedding(
            query_vector=query_vector,
            embedder_path="test/embedder",
            top_k=2,
        )

        # Should return results
        self.assertGreater(len(results), 0)

        # Results should have similarity_score annotation
        for msg in results:
            self.assertTrue(hasattr(msg, "similarity_score"))
            self.assertIsNotNone(msg.similarity_score)

    def test_core_message_vector_store(self):
        """Test CoreChatMessageVectorStore functionality."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)

        # Should return messages
        self.assertGreater(len(results), 0)

        # Check that results are MessageSearchResult objects
        for result in results:
            self.assertIsNotNone(result.message)
            self.assertIsNotNone(result.similarity_score)

    def test_message_type_filter(self):
        """Test filtering by message type."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            msg_type="HUMAN",
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        message_ids = {result.message.id for result in results}

        # Should include HUMAN messages
        self.assertIn(self.msg1.id, message_ids)
        self.assertIn(self.msg3.id, message_ids)

        # Should NOT include LLM messages
        self.assertNotIn(self.msg2.id, message_ids)

    def test_conversation_filter(self):
        """Test filtering by conversation."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            conversation_id=self.conversation.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)

        # All results should be from the specified conversation
        for result in results:
            self.assertEqual(result.message.conversation_id, self.conversation.id)

    def test_message_search_with_query_text(self):
        """Test message search using query_text (generates embedding)."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_text="test message search",
            similarity_top_k=10,
        )

        # Will fail without real embedder, but tests the code path
        try:
            results = store.search(query)
            self.assertIsInstance(results, list)
        except (ValueError, AttributeError, TypeError) as e:
            # Expected without real embedder service
            error_msg = str(e).lower()
            self.assertTrue(
                "len(" in error_msg or "embedder" in error_msg or "vector" in error_msg
            )

    def test_message_search_nonexistent_user(self):
        """Test message search with nonexistent user."""
        store = CoreChatMessageVectorStore(
            user_id=99999,  # Non-existent user
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)

        # Should return empty results, not crash
        self.assertEqual(len(results), 0)

    def test_message_search_missing_query_raises_error(self):
        """Test that message search raises ValueError when neither text nor embedding provided."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            similarity_top_k=10,
        )

        with self.assertRaises(ValueError) as ctx:
            store.search(query)

        self.assertIn("Either query_text or query_embedding", str(ctx.exception))


class GraphQLConversationSearchTest(TestCase):
    """Test GraphQL search queries."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="graphql_search_user", password="testpassword"
        )
        self.graphene_client = Client(schema, context_value=TestContext(self.user))

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="GraphQL Search Corpus", creator=self.user
        )

        # Create conversation
        self.conversation = Conversation.objects.create(
            title="GraphQL Test Conversation",
            description="Testing GraphQL search",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conversation,
            permissions=[PermissionTypes.ALL],
        )

        # Create message
        self.message = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="Test message content for search",
        )

        # Create embeddings
        Embedding.objects.create(
            conversation=self.conversation,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        Embedding.objects.create(
            message=self.message,
            embedder_path="test/embedder",
            vector_384=[0.2] * 384,
            creator=self.user,
        )

    def test_search_conversations_query(self):
        """Test the searchConversations GraphQL query with pagination."""
        query = """
            query SearchConversations($query: String!, $corpusId: ID, $topK: Int, $first: Int, $after: String) {
                searchConversations(query: $query, corpusId: $corpusId, topK: $topK, first: $first, after: $after) {
                    edges {
                        node {
                            id
                            title
                            description
                            conversationType
                        }
                        cursor
                    }
                    pageInfo {
                        hasNextPage
                        hasPreviousPage
                        startCursor
                        endCursor
                    }
                    totalCount
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test search query",
                "corpusId": corpus_global_id,
                "topK": 5,
                "first": 2,
            },
        )

        # In test environment, embedding generation from text may fail without real embedder service
        # This is expected - we're mainly testing that the GraphQL resolver structure is correct
        # The actual embedding search is tested in the vector store unit tests
        if result.get("errors"):
            # Verify it's an embedding-related error (not a permission or other critical error)
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len()" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error type: {error_message}",
            )
        else:
            # If embedder service is available, verify response structure
            self.assertIsNotNone(result.get("data"))
            search_result = result["data"]["searchConversations"]

            # Verify pagination structure
            self.assertIn("edges", search_result)
            self.assertIn("pageInfo", search_result)
            self.assertIn("totalCount", search_result)

            # Verify pageInfo fields
            page_info = search_result["pageInfo"]
            self.assertIn("hasNextPage", page_info)
            self.assertIn("hasPreviousPage", page_info)
            self.assertIn("startCursor", page_info)
            self.assertIn("endCursor", page_info)

            # Verify edges structure
            edges = search_result["edges"]
            self.assertIsInstance(edges, list)
            if len(edges) > 0:
                self.assertIn("node", edges[0])
                self.assertIn("cursor", edges[0])

    def test_search_conversations_pagination_with_cursor(self):
        """Test searchConversations pagination with after cursor."""
        query = """
            query SearchConversations($query: String!, $corpusId: ID, $first: Int, $after: String) {
                searchConversations(query: $query, corpusId: $corpusId, first: $first, after: $after) {
                    edges {
                        node {
                            id
                            title
                        }
                        cursor
                    }
                    pageInfo {
                        hasNextPage
                        endCursor
                    }
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        # First request - get first page
        first_result = self.graphene_client.execute(
            query,
            variables={
                "query": "test query",
                "corpusId": corpus_global_id,
                "first": 1,
            },
        )

        # Skip test if embedder not available (expected in test environment)
        if first_result.get("errors"):
            error_message = first_result["errors"][0]["message"]
            self.assertTrue(
                "len()" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )
            return

        # If results available, test cursor pagination
        if first_result["data"]["searchConversations"]["pageInfo"]["hasNextPage"]:
            end_cursor = first_result["data"]["searchConversations"]["pageInfo"][
                "endCursor"
            ]

            # Second request - get next page using cursor
            second_result = self.graphene_client.execute(
                query,
                variables={
                    "query": "test query",
                    "corpusId": corpus_global_id,
                    "first": 1,
                    "after": end_cursor,
                },
            )

            self.assertIsNotNone(second_result.get("data"))
            second_edges = second_result["data"]["searchConversations"]["edges"]

            # Verify second page has different results than first page
            if len(second_edges) > 0:
                first_id = first_result["data"]["searchConversations"]["edges"][0][
                    "node"
                ]["id"]
                second_id = second_edges[0]["node"]["id"]
                self.assertNotEqual(
                    first_id,
                    second_id,
                    "Cursor pagination should return different results",
                )

    def test_search_messages_query(self):
        """Test the searchMessages GraphQL query."""
        query = """
            query SearchMessages($query: String!, $conversationId: ID, $topK: Int) {
                searchMessages(query: $query, conversationId: $conversationId, topK: $topK) {
                    id
                    content
                    msgType
                }
            }
        """

        conversation_global_id = to_global_id("ConversationType", self.conversation.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test message query",
                "conversationId": conversation_global_id,
                "topK": 5,
            },
        )

        # In test environment, embedding generation from text may fail without real embedder service
        # This is expected - we're mainly testing that the GraphQL resolver structure is correct
        # The actual embedding search is tested in the vector store unit tests
        if result.get("errors"):
            # Verify it's an embedding-related error (not a permission or other critical error)
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len()" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error type: {error_message}",
            )
        else:
            # If embedder service is available, verify response structure
            self.assertIsNotNone(result.get("data"))
            messages = result["data"]["searchMessages"]
            self.assertIsInstance(messages, list)

    def test_search_requires_authentication(self):
        """Test that search queries require authentication."""
        # Create client without authenticated user
        anonymous_client = Client(schema, context_value=TestContext(None))

        query = """
            query SearchConversations($query: String!) {
                searchConversations(query: $query) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        result = anonymous_client.execute(
            query,
            variables={"query": "test"},
        )

        # Should have errors (not authenticated)
        self.assertIsNotNone(result.get("errors"))

    def test_search_conversations_with_document_filter(self):
        """Test searchConversations with document_id filter."""
        pdf_file = ContentFile(b"%PDF-1.4 test pdf", name="doc_filter.pdf")
        doc = Document.objects.create(
            creator=self.user,
            title="Document Filter Test",
            pdf_file=pdf_file,
            backend_lock=True,
        )

        # Create document-linked conversation
        doc_conv = Conversation.objects.create(
            title="Document Conversation",
            chat_with_document=doc,
            creator=self.user,
            conversation_type="chat",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=doc_conv,
            permissions=[PermissionTypes.ALL],
        )

        Embedding.objects.create(
            conversation=doc_conv,
            embedder_path="test/embedder",
            vector_384=[0.3] * 384,
            creator=self.user,
        )

        query = """
            query SearchConversations($query: String!, $documentId: ID) {
                searchConversations(query: $query, documentId: $documentId) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        doc_global_id = to_global_id("DocumentType", doc.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test document query",
                "documentId": doc_global_id,
            },
        )

        # May fail without real embedder, but tests the resolver path
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )

    def test_search_messages_with_corpus_filter(self):
        """Test searchMessages with corpus filter."""
        query = """
            query SearchMessages($query: String!, $corpusId: ID) {
                searchMessages(query: $query, corpusId: $corpusId) {
                    id
                    content
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test message query",
                "corpusId": corpus_global_id,
            },
        )

        # May fail without real embedder, but tests the resolver path
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )

    def test_search_messages_with_msg_type_filter(self):
        """Test searchMessages with msg_type filter."""
        query = """
            query SearchMessages($query: String!, $conversationId: ID, $msgType: String) {
                searchMessages(
                    query: $query,
                    conversationId: $conversationId,
                    msgType: $msgType
                ) {
                    id
                    content
                    msgType
                }
            }
        """

        conversation_global_id = to_global_id("ConversationType", self.conversation.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test message",
                "conversationId": conversation_global_id,
                "msgType": "HUMAN",
            },
        )

        # May fail without real embedder
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )

    def test_search_with_custom_top_k(self):
        """Test search queries with custom top_k parameter."""
        query = """
            query SearchConversations($query: String!, $corpusId: ID, $topK: Int) {
                searchConversations(query: $query, corpusId: $corpusId, topK: $topK) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test",
                "corpusId": corpus_global_id,
                "topK": 5,
            },
        )

        # May fail without real embedder
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )


class ConversationPermissionTest(TestCase):
    """Test permission filtering in conversation search using visible_to_user() pattern."""

    def setUp(self):
        """Set up test users and conversations with various permission scenarios."""
        # Create users
        self.user1 = User.objects.create_user(username="user1", password="testpassword")
        self.user2 = User.objects.create_user(username="user2", password="testpassword")
        self.superuser = User.objects.create_superuser(
            username="superuser", password="testpassword", email="super@test.com"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Permission Test Corpus", creator=self.user1
        )

        # Create conversations with different visibility scenarios

        # 1. User1's private conversation (creator visibility)
        self.user1_private_conv = Conversation.objects.create(
            title="User1 Private Thread",
            description="Only visible to creator",
            chat_with_corpus=self.corpus,
            creator=self.user1,
            conversation_type="thread",
            is_public=False,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user1,
            instance=self.user1_private_conv,
            permissions=[PermissionTypes.ALL],
        )

        # 2. User1's public conversation (public visibility)
        self.user1_public_conv = Conversation.objects.create(
            title="User1 Public Thread",
            description="Visible to all users",
            chat_with_corpus=self.corpus,
            creator=self.user1,
            conversation_type="thread",
            is_public=True,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user1,
            instance=self.user1_public_conv,
            permissions=[PermissionTypes.ALL],
        )

        # 3. User1's private conversation shared with User2 (guardian permissions)
        self.shared_conv = Conversation.objects.create(
            title="Shared Thread",
            description="Shared explicitly via guardian permissions",
            chat_with_corpus=self.corpus,
            creator=self.user1,
            conversation_type="thread",
            is_public=False,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user1,
            instance=self.shared_conv,
            permissions=[PermissionTypes.ALL],
        )
        # Share with user2
        set_permissions_for_obj_to_user(
            user_val=self.user2,
            instance=self.shared_conv,
            permissions=[PermissionTypes.READ],
        )

        # 4. User2's completely private conversation (not accessible to user1)
        # Note: We use CHAT type (not THREAD) because THREAD type conversations
        # inherit visibility from their corpus - anyone who can see the corpus
        # can see its threads. CHAT type uses restrictive permissions.
        self.user2_private_conv = Conversation.objects.create(
            title="User2 Private Chat",
            description="Not visible to user1",
            chat_with_corpus=self.corpus,
            creator=self.user2,
            conversation_type="chat",
            is_public=False,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user2,
            instance=self.user2_private_conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create embeddings for all conversations
        for conv in [
            self.user1_private_conv,
            self.user1_public_conv,
            self.shared_conv,
            self.user2_private_conv,
        ]:
            Embedding.objects.create(
                conversation=conv,
                embedder_path="test/embedder",
                vector_384=[0.1] * 384,
                creator=conv.creator,
            )

    def test_creator_sees_own_conversations(self):
        """Test that creator can see their own conversations."""
        store = CoreConversationVectorStore(
            user_id=self.user1.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # User1 should see all their own conversations
        self.assertIn(self.user1_private_conv.id, conversation_ids)
        self.assertIn(self.user1_public_conv.id, conversation_ids)
        self.assertIn(self.shared_conv.id, conversation_ids)

        # User1 should NOT see user2's private conversation
        self.assertNotIn(self.user2_private_conv.id, conversation_ids)

    def test_guardian_permission_sharing(self):
        """Test that explicitly shared conversations appear in search."""
        store = CoreConversationVectorStore(
            user_id=self.user2.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # User2 should see shared conversation (via guardian permissions)
        self.assertIn(self.shared_conv.id, conversation_ids)

        # User2 should see public conversation
        self.assertIn(self.user1_public_conv.id, conversation_ids)

        # User2 should see their own private conversation
        self.assertIn(self.user2_private_conv.id, conversation_ids)

        # User2 should NOT see user1's other private conversation
        self.assertNotIn(self.user1_private_conv.id, conversation_ids)

    def test_public_conversations_visible_to_all(self):
        """Test that public conversations are visible to all authenticated users."""
        # User2 should see user1's public conversation
        store = CoreConversationVectorStore(
            user_id=self.user2.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should see public conversation
        self.assertIn(self.user1_public_conv.id, conversation_ids)

    def test_permission_revocation(self):
        """Test that revoking permissions excludes conversations from search."""
        from guardian.shortcuts import remove_perm

        # Initially, user2 should see shared conversation
        store = CoreConversationVectorStore(
            user_id=self.user2.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}
        self.assertIn(self.shared_conv.id, conversation_ids)

        # Revoke permission
        remove_perm("read_conversation", self.user2, self.shared_conv)

        # Now user2 should NOT see shared conversation
        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}
        self.assertNotIn(self.shared_conv.id, conversation_ids)

    def test_superuser_has_no_blanket_access(self):
        """Scoped admin access (2026-05): a no-grant superuser is computed like
        a normal user. The corpus is private and the superuser is neither
        creator nor grantee, so it sees ONLY the public conversation — not the
        private, shared-with-someone-else, or other user's private chats.
        Granting READ on one private conversation exposes just that one."""
        store = CoreConversationVectorStore(
            user_id=self.superuser.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Public conversation is visible to everyone.
        self.assertIn(self.user1_public_conv.id, conversation_ids)
        # No-grant superuser does NOT see private / shared-elsewhere / other
        # user's conversations on a private corpus.
        self.assertNotIn(self.user1_private_conv.id, conversation_ids)
        self.assertNotIn(self.shared_conv.id, conversation_ids)
        self.assertNotIn(self.user2_private_conv.id, conversation_ids)

        # Positive path: an explicit guardian READ grant exposes exactly that
        # one private conversation through the normal visibility computation.
        set_permissions_for_obj_to_user(
            user_val=self.superuser,
            instance=self.user1_private_conv,
            permissions=[PermissionTypes.READ],
        )
        results_after = store.search(query)
        ids_after = {result.conversation.id for result in results_after}
        self.assertIn(self.user1_private_conv.id, ids_after)
        # Still does NOT see the conversation shared only with user2.
        self.assertNotIn(self.shared_conv.id, ids_after)

    def test_anonymous_user_public_only(self):
        """Test that anonymous users only see public conversations."""
        # Anonymous user (no user_id)
        store = CoreConversationVectorStore(
            user_id=None,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Anonymous should ONLY see public conversations
        self.assertIn(self.user1_public_conv.id, conversation_ids)

        # Anonymous should NOT see private or shared conversations
        self.assertNotIn(self.user1_private_conv.id, conversation_ids)
        self.assertNotIn(self.shared_conv.id, conversation_ids)
        self.assertNotIn(self.user2_private_conv.id, conversation_ids)

    def test_soft_deleted_conversations_excluded(self):
        """Test that soft-deleted conversations are excluded from search."""
        from django.utils import timezone

        # Soft delete a conversation
        self.user1_private_conv.deleted_at = timezone.now()
        self.user1_private_conv.save()

        store = CoreConversationVectorStore(
            user_id=self.user1.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should NOT include soft-deleted conversation
        self.assertNotIn(self.user1_private_conv.id, conversation_ids)


class SearchConversationsResolverCoverageTest(TestCase):
    """Tests to improve coverage of searchConversations resolver code paths."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="resolver_test_user", password="testpassword"
        )
        self.corpus = Corpus.objects.create(
            title="Resolver Test Corpus", creator=self.user
        )

        # Create conversation with embedding
        self.conv = Conversation.objects.create(
            title="Test Conversation",
            description="Test description",
            chat_with_corpus=self.corpus,
            creator=self.user,
            conversation_type="thread",
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create embedding for the conversation
        Embedding.objects.create(
            conversation=self.conv,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        self.graphene_client = Client(schema, context_value=TestContext(self.user))

    def test_search_conversations_with_conversation_type_filter(self):
        """Test searchConversations with conversation_type filter."""
        query = """
            query SearchConversations($query: String!, $conversationType: String) {
                searchConversations(query: $query, conversationType: $conversationType) {
                    edges {
                        node {
                            id
                            title
                            conversationType
                        }
                    }
                }
            }
        """

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test query",
                "conversationType": "thread",
            },
        )

        # Test passes if it hits the code path (may fail on embedder)
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )

    def test_search_with_only_document_id(self):
        """Test search with only document_id (no corpus_id)."""
        pdf_file = ContentFile(b"%PDF-1.4 test pdf", name="doc_only.pdf")
        doc = Document.objects.create(
            creator=self.user,
            title="Document Only Test",
            pdf_file=pdf_file,
            backend_lock=True,
        )

        query = """
            query SearchConversations($query: String!, $documentId: ID) {
                searchConversations(query: $query, documentId: $documentId) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        doc_global_id = to_global_id("DocumentType", doc.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test document search",
                "documentId": doc_global_id,
            },
        )

        # Test passes if it hits the code path (may fail on embedder)
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )

    def test_search_with_mocked_results_anonymous_user(self):
        """Test searchConversations with mocked vector store returns for anonymous user."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            ConversationSearchResult,
        )

        # Create anonymous client
        anon_client = Client(schema, context_value=TestContext(None))

        query = """
            query SearchConversations($query: String!, $corpusId: ID, $first: Int) {
                searchConversations(query: $query, corpusId: $corpusId, first: $first) {
                    edges {
                        node {
                            id
                            title
                        }
                        cursor
                    }
                    pageInfo {
                        hasNextPage
                        hasPreviousPage
                    }
                    totalCount
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        # Create mock search result
        mock_result = ConversationSearchResult(
            conversation=self.conv,
            similarity_score=0.95,
        )

        # Mock the vector store
        mock_store = Mock()
        mock_store.search.return_value = [mock_result]

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreConversationVectorStore",
            return_value=mock_store,
        ):
            result = anon_client.execute(
                query,
                variables={
                    "query": "test query",
                    "corpusId": corpus_global_id,
                    "first": 10,
                },
            )

            # Should succeed and return results
            self.assertIsNone(result.get("errors"))
            self.assertIsNotNone(result.get("data"))
            search_result = result["data"]["searchConversations"]

            # Verify structure
            self.assertIn("edges", search_result)
            self.assertIn("pageInfo", search_result)
            self.assertIn("totalCount", search_result)

            # Verify we got the conversation
            self.assertEqual(len(search_result["edges"]), 1)
            node = search_result["edges"][0]["node"]
            self.assertEqual(node["title"], "Test Conversation")

    def test_search_with_mocked_results_authenticated_user(self):
        """Test searchConversations with mocked vector store returns for authenticated user."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            ConversationSearchResult,
        )

        query = """
            query SearchConversations($query: String!, $corpusId: ID) {
                searchConversations(query: $query, corpusId: $corpusId) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                    totalCount
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)

        # Create mock search result
        mock_result = ConversationSearchResult(
            conversation=self.conv,
            similarity_score=0.95,
        )

        # Mock the vector store
        mock_store = Mock()
        mock_store.search.return_value = [mock_result]

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreConversationVectorStore",
            return_value=mock_store,
        ) as mock_vector_store_class:
            result = self.graphene_client.execute(
                query,
                variables={
                    "query": "test query",
                    "corpusId": corpus_global_id,
                },
            )

            # Should succeed and return results
            self.assertIsNone(result.get("errors"))
            self.assertIsNotNone(result.get("data"))

            # Verify vector store was called with correct user_id
            mock_vector_store_class.assert_called_once()
            call_kwargs = mock_vector_store_class.call_args[1]
            self.assertEqual(call_kwargs["user_id"], self.user.id)

            # Verify search was called
            mock_store.search.assert_called_once()

            # Verify results
            search_result = result["data"]["searchConversations"]
            self.assertEqual(search_result["totalCount"], 1)
            self.assertEqual(len(search_result["edges"]), 1)

    def test_search_conversations_with_all_filters(self):
        """Test searchConversations with all filter parameters."""
        pdf_file = ContentFile(b"%PDF-1.4 test pdf", name="all_filters.pdf")
        doc = Document.objects.create(
            creator=self.user,
            title="All Filters Test Doc",
            pdf_file=pdf_file,
            backend_lock=True,
        )

        query = """
            query SearchConversations(
                $query: String!,
                $corpusId: ID,
                $documentId: ID,
                $conversationType: String,
                $topK: Int
            ) {
                searchConversations(
                    query: $query,
                    corpusId: $corpusId,
                    documentId: $documentId,
                    conversationType: $conversationType,
                    topK: $topK
                ) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        corpus_global_id = to_global_id("CorpusType", self.corpus.id)
        doc_global_id = to_global_id("DocumentType", doc.id)

        result = self.graphene_client.execute(
            query,
            variables={
                "query": "test query with all params",
                "corpusId": corpus_global_id,
                "documentId": doc_global_id,
                "conversationType": "thread",
                "topK": 50,
            },
        )

        # Test passes if it hits the code path (may fail on embedder)
        if result.get("errors"):
            error_message = result["errors"][0]["message"]
            self.assertTrue(
                "len(" in error_message or "embedder" in error_message.lower(),
                f"Unexpected error: {error_message}",
            )


class MessagePermissionTest(TestCase):
    """Test that message search inherits conversation visibility."""

    def setUp(self):
        """Set up test data."""
        # Create users
        self.user1 = User.objects.create_user(
            username="msg_user1", password="testpassword"
        )
        self.user2 = User.objects.create_user(
            username="msg_user2", password="testpassword"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Message Permission Corpus", creator=self.user1
        )

        # Create user1's private conversation
        self.user1_private_conv = Conversation.objects.create(
            title="User1 Private Chat",
            chat_with_corpus=self.corpus,
            creator=self.user1,
            conversation_type="thread",
            is_public=False,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user1,
            instance=self.user1_private_conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create user1's public conversation
        self.user1_public_conv = Conversation.objects.create(
            title="User1 Public Chat",
            chat_with_corpus=self.corpus,
            creator=self.user1,
            conversation_type="thread",
            is_public=True,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user1,
            instance=self.user1_public_conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create messages in private conversation
        self.private_msg1 = ChatMessage.objects.create(
            conversation=self.user1_private_conv,
            creator=self.user1,
            msg_type="HUMAN",
            content="Private message 1",
        )

        # Create messages in public conversation
        self.public_msg1 = ChatMessage.objects.create(
            conversation=self.user1_public_conv,
            creator=self.user1,
            msg_type="HUMAN",
            content="Public message 1",
        )

        # Create embeddings
        Embedding.objects.create(
            message=self.private_msg1,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user1,
        )
        Embedding.objects.create(
            message=self.public_msg1,
            embedder_path="test/embedder",
            vector_384=[0.2] * 384,
            creator=self.user1,
        )

    def test_message_search_inherits_conversation_visibility(self):
        """Test that messages are only visible if their conversation is visible."""
        # User2 searching for messages
        store = CoreChatMessageVectorStore(
            user_id=self.user2.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        message_ids = {result.message.id for result in results}

        # User2 should see messages from public conversation
        self.assertIn(self.public_msg1.id, message_ids)

        # User2 should NOT see messages from private conversation
        self.assertNotIn(self.private_msg1.id, message_ids)

    def test_creator_sees_own_messages(self):
        """Test that creator can see messages in their own conversations."""
        store = CoreChatMessageVectorStore(
            user_id=self.user1.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        message_ids = {result.message.id for result in results}

        # User1 should see messages from both conversations
        self.assertIn(self.private_msg1.id, message_ids)
        self.assertIn(self.public_msg1.id, message_ids)


@pytest.mark.django_db
class AsyncConversationSearchTest(TestCase):
    """Test async search methods in CoreConversationVectorStore."""

    def setUp(self):
        """Set up test data."""
        # Create user
        self.user = User.objects.create_user(
            username="async_conv_user", password="testpassword"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Async Conversation Test Corpus", creator=self.user
        )

        # Create conversations
        self.conversation1 = Conversation.objects.create(
            title="Async Test Conversation 1",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conversation1,
            permissions=[PermissionTypes.ALL],
        )

        self.conversation2 = Conversation.objects.create(
            title="Async Test Conversation 2",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conversation2,
            permissions=[PermissionTypes.ALL],
        )

        # Create embeddings
        Embedding.objects.create(
            conversation=self.conversation1,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        Embedding.objects.create(
            conversation=self.conversation2,
            embedder_path="test/embedder",
            vector_384=[0.2] * 384,
            creator=self.user,
        )

    @pytest.mark.asyncio
    async def test_async_conversation_search(self):
        """Test CoreConversationVectorStore.async_search method."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should return results
        self.assertGreater(len(results), 0)

        # Verify conversation IDs
        conversation_ids = {r.conversation.id for r in results}
        self.assertIn(self.conversation1.id, conversation_ids)
        self.assertIn(self.conversation2.id, conversation_ids)

        # Verify similarity scores are present
        for result in results:
            self.assertIsNotNone(result.similarity_score)
            self.assertIsInstance(result.similarity_score, float)

    @pytest.mark.asyncio
    async def test_async_conversation_search_with_filters(self):
        """Test async search with additional filters."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            conversation_type="thread",
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should work with type filter
        self.assertIsInstance(results, list)

    @pytest.mark.asyncio
    async def test_async_conversation_search_nonexistent_user(self):
        """Test async search gracefully handles nonexistent user."""
        store = CoreConversationVectorStore(
            user_id=99999,  # Non-existent user
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should return empty results, not crash
        self.assertEqual(len(results), 0)

    @pytest.mark.asyncio
    async def test_async_conversation_search_with_top_k(self):
        """Test async search respects top_k parameter."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=1,  # Limit to 1 result
        )

        results = await store.async_search(query)

        # Should respect top_k limit
        self.assertLessEqual(len(results), 1)

    @pytest.mark.asyncio
    async def test_async_search_with_query_text(self):
        """Test async search using query_text (generates embedding)."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_text="async test conversation",
            similarity_top_k=10,
        )

        # Will fail without real embedder, but tests the code path
        try:
            results = await store.async_search(query)
            self.assertIsInstance(results, list)
        except (ValueError, AttributeError, TypeError) as e:
            # Expected without real embedder service
            error_msg = str(e).lower()
            self.assertTrue(
                "len(" in error_msg or "embedder" in error_msg or "vector" in error_msg
            )

    @pytest.mark.asyncio
    async def test_async_search_missing_query_raises_error(self):
        """Test that async search raises ValueError when neither text nor embedding provided."""
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            similarity_top_k=10,
        )

        with self.assertRaises(ValueError) as ctx:
            await store.async_search(query)

        self.assertIn("Either query_text or query_embedding", str(ctx.exception))


@pytest.mark.django_db
class AsyncMessageSearchTest(TestCase):
    """Test async search methods in CoreChatMessageVectorStore."""

    def setUp(self):
        """Set up test data."""
        # Create user
        self.user = User.objects.create_user(
            username="async_msg_user", password="testpassword"
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Async Message Test Corpus", creator=self.user
        )

        # Create conversation
        self.conversation = Conversation.objects.create(
            title="Async Message Test Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conversation,
            permissions=[PermissionTypes.ALL],
        )

        # Create messages
        self.message1 = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="First async test message",
        )

        self.message2 = ChatMessage.objects.create(
            conversation=self.conversation,
            creator=self.user,
            msg_type="LLM",
            content="Second async test message",
        )

        # Create embeddings
        Embedding.objects.create(
            message=self.message1,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        Embedding.objects.create(
            message=self.message2,
            embedder_path="test/embedder",
            vector_384=[0.2] * 384,
            creator=self.user,
        )

    @pytest.mark.asyncio
    async def test_async_message_search(self):
        """Test CoreChatMessageVectorStore.async_search method."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should return results
        self.assertGreater(len(results), 0)

        # Verify message IDs
        message_ids = {r.message.id for r in results}
        self.assertIn(self.message1.id, message_ids)
        self.assertIn(self.message2.id, message_ids)

        # Verify similarity scores
        for result in results:
            self.assertIsNotNone(result.similarity_score)
            self.assertIsInstance(result.similarity_score, float)

    @pytest.mark.asyncio
    async def test_async_message_search_with_msg_type_filter(self):
        """Test async message search with message type filter."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            msg_type="HUMAN",
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should only return HUMAN messages
        for result in results:
            self.assertEqual(result.message.msg_type, "HUMAN")

    @pytest.mark.asyncio
    async def test_async_message_search_with_conversation_filter(self):
        """Test async message search with conversation filter."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            conversation_id=self.conversation.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # All results should be from the specified conversation
        for result in results:
            self.assertEqual(result.message.conversation_id, self.conversation.id)

    @pytest.mark.asyncio
    async def test_async_message_search_nonexistent_user(self):
        """Test async message search with nonexistent user."""
        store = CoreChatMessageVectorStore(
            user_id=99999,  # Non-existent user
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = await store.async_search(query)

        # Should return empty results, not crash
        self.assertEqual(len(results), 0)

    @pytest.mark.asyncio
    async def test_async_message_search_with_query_text(self):
        """Test async message search using query_text (generates embedding)."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_text="async test message",
            similarity_top_k=10,
        )

        # Will fail without real embedder, but tests the code path
        try:
            results = await store.async_search(query)
            self.assertIsInstance(results, list)
        except (ValueError, AttributeError, TypeError) as e:
            # Expected without real embedder service
            error_msg = str(e).lower()
            self.assertTrue(
                "len(" in error_msg or "embedder" in error_msg or "vector" in error_msg
            )

    @pytest.mark.asyncio
    async def test_async_message_search_missing_query_raises_error(self):
        """Test that async message search raises ValueError when query missing."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            similarity_top_k=10,
        )

        with self.assertRaises(ValueError) as ctx:
            await store.async_search(query)

        self.assertIn("Either query_text or query_embedding", str(ctx.exception))


class VectorStoreEdgeCasesTest(TestCase):
    """Test edge cases and error paths in vector stores."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="edge_case_user", password="testpassword"
        )
        self.corpus = Corpus.objects.create(title="Edge Case Corpus", creator=self.user)

    def test_conversation_store_requires_corpus_or_embedder(self):
        """Test that CoreConversationVectorStore requires either corpus_id or embedder_path."""
        with self.assertRaises(ValueError) as ctx:
            CoreConversationVectorStore(
                user_id=self.user.id,
                # No corpus_id or embedder_path provided
            )

        self.assertIn("requires either 'corpus_id'", str(ctx.exception))

    def test_message_store_requires_corpus_or_embedder(self):
        """Test that CoreChatMessageVectorStore requires either corpus_id or embedder_path."""
        with self.assertRaises(ValueError) as ctx:
            CoreChatMessageVectorStore(
                user_id=self.user.id,
                # No corpus_id or embedder_path provided
            )

        self.assertIn("requires either 'corpus_id'", str(ctx.exception))

    def test_conversation_store_with_invalid_embed_dim(self):
        """Test conversation store handles invalid embed dimensions gracefully."""
        # Create store with invalid dimension (not 384, 768, 1536, or 3072)
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
            embed_dim=512,  # Invalid dimension
        )

        # Should fall back to default dimension (384)
        self.assertEqual(store.embed_dim, 384)

    def test_message_store_with_invalid_embed_dim(self):
        """Test message store handles invalid embed dimensions gracefully."""
        store = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
            embed_dim=999,  # Invalid dimension
        )

        # Should fall back to default dimension (384)
        self.assertEqual(store.embed_dim, 384)

    def test_conversation_store_with_document_id_filter(self):
        """Test conversation store filtering by document_id."""
        pdf_file = ContentFile(b"%PDF-1.4 test", name="doc_test.pdf")
        doc = Document.objects.create(
            creator=self.user,
            title="Test Document",
            pdf_file=pdf_file,
            backend_lock=True,
        )

        # Create document-linked conversation
        conv = Conversation.objects.create(
            title="Document Conversation",
            chat_with_document=doc,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv,
            permissions=[PermissionTypes.ALL],
        )

        Embedding.objects.create(
            conversation=conv,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        # Create store with document_id filter
        store = CoreConversationVectorStore(
            user_id=self.user.id,
            document_id=doc.id,
            embedder_path="test/embedder",
        )

        query = VectorSearchQuery(
            query_embedding=[0.1] * 384,
            similarity_top_k=10,
        )

        results = store.search(query)
        conversation_ids = {result.conversation.id for result in results}

        # Should include document-linked conversation
        self.assertIn(conv.id, conversation_ids)

    def test_message_store_exclude_deleted_parameter(self):
        """Test message store exclude_deleted parameter exists and can be set."""
        # Test that exclude_deleted parameter can be set to False
        store_include = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
            exclude_deleted=False,
        )

        # Verify the parameter is set correctly
        self.assertEqual(store_include.exclude_deleted, False)

        # Test default value (True)
        store_default = CoreChatMessageVectorStore(
            user_id=self.user.id,
            corpus_id=self.corpus.id,
            embedder_path="test/embedder",
        )

        self.assertEqual(store_default.exclude_deleted, True)


class ConversationModelVisibilityTest(TestCase):
    """Test SoftDeleteQuerySet.visible_to_user edge cases."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="visibility_user", password="testpassword"
        )
        self.corpus = Corpus.objects.create(
            title="Visibility Test Corpus", creator=self.user
        )

    def test_visible_to_user_with_none_user(self):
        """Test visible_to_user handles None user (anonymous).

        Per the permission model:
        - Anonymous users can only see THREADs (not CHATs)
        - Context inheritance: threads on public corpuses are visible
        - Thread's own is_public flag provides direct access, but
          context inheritance works independently
        - Threads on PRIVATE corpuses are NOT visible to anonymous
        """
        # Make corpus public for context inheritance
        self.corpus.is_public = True
        self.corpus.save()

        # Create a private corpus
        private_corpus = Corpus.objects.create(
            title="Private Corpus", creator=self.user, is_public=False
        )

        # Create thread on public corpus (is_public=True)
        public_thread = Conversation.objects.create(
            title="Public Thread",
            conversation_type="thread",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=True,
        )

        # Create thread on public corpus (is_public=False)
        # This SHOULD be visible via context inheritance
        inherited_thread = Conversation.objects.create(
            title="Inherited Thread",
            conversation_type="thread",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=False,
        )

        # Create thread on PRIVATE corpus - should NOT be visible
        private_corpus_thread = Conversation.objects.create(
            title="Private Corpus Thread",
            conversation_type="thread",
            chat_with_corpus=private_corpus,
            creator=self.user,
            is_public=False,
        )

        # Query with None user (anonymous)
        visible = Conversation.objects.visible_to_user(None)

        visible_ids = set(visible.values_list("id", flat=True))

        # Both threads on public corpus should be visible
        self.assertIn(public_thread.id, visible_ids)
        self.assertIn(inherited_thread.id, visible_ids)

        # Thread on private corpus should NOT be visible
        self.assertNotIn(private_corpus_thread.id, visible_ids)

    def test_visible_to_user_fallback_no_permission_model(self):
        """Test visible_to_user fallback when permission model doesn't exist."""
        # This tests the LookupError exception handling in visible_to_user
        # The code handles cases where guardian permission models don't exist

        # Create conversation
        conv = Conversation.objects.create(
            title="Test Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=False,
        )

        # Query as creator
        visible = Conversation.objects.visible_to_user(self.user)
        visible_ids = set(visible.values_list("id", flat=True))

        # Creator should see their own conversation
        self.assertIn(conv.id, visible_ids)

    def test_visible_to_user_superuser_has_no_blanket_access(self):
        """Scoped admin access (2026-05): a no-grant superuser is computed like
        a normal user. It sees a public conversation but NOT a private
        conversation owned by another user on a private corpus. Granting READ
        via the normal path makes the private one visible."""
        # Create a superuser (stranger: not creator, no grants).
        superuser = User.objects.create_superuser(
            username="superuser_admin", password="admin", email="superuser@test.com"
        )

        # Create private conversation owned by regular user (on a private corpus).
        private_conv = Conversation.objects.create(
            title="Private Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=False,
        )

        # Create public conversation
        public_conv = Conversation.objects.create(
            title="Public Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=True,
        )

        # No-grant superuser sees only the public conversation, not the private.
        visible = Conversation.objects.visible_to_user(superuser)
        visible_ids = set(visible.values_list("id", flat=True))

        self.assertIn(public_conv.id, visible_ids)
        self.assertNotIn(private_conv.id, visible_ids)

        # Positive path: an explicit guardian READ grant exposes the private
        # conversation through the normal visibility computation.
        set_permissions_for_obj_to_user(
            user_val=superuser,
            instance=private_conv,
            permissions=[PermissionTypes.READ],
        )
        visible_after = Conversation.objects.visible_to_user(superuser)
        self.assertIn(private_conv.id, set(visible_after.values_list("id", flat=True)))

    def test_visible_to_user_with_shared_permissions(self):
        """Test that users see conversations explicitly shared with them."""
        # Create second user
        other_user = User.objects.create_user(
            username="other_user", password="testpass"
        )

        # Create private conversation owned by first user
        conv = Conversation.objects.create(
            title="Shared Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=False,
        )

        # Share with other_user
        set_permissions_for_obj_to_user(
            user_val=other_user,
            instance=conv,
            permissions=[PermissionTypes.READ],
        )

        # other_user should be able to see it
        visible = Conversation.objects.visible_to_user(other_user)
        visible_ids = set(visible.values_list("id", flat=True))

        self.assertIn(conv.id, visible_ids)

        # But they shouldn't see conversations they don't have access to
        private_conv = Conversation.objects.create(
            title="Not Shared",
            chat_with_corpus=self.corpus,
            creator=self.user,
            is_public=False,
        )

        visible2 = Conversation.objects.visible_to_user(other_user)
        visible_ids2 = set(visible2.values_list("id", flat=True))

        self.assertIn(conv.id, visible_ids2)  # Still sees shared one
        self.assertNotIn(private_conv.id, visible_ids2)  # Doesn't see unshared


class EmbeddingDimensionTest(TestCase):
    """Test vector search with different embedding dimensions."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="dimension_user", password="testpassword"
        )
        self.corpus = Corpus.objects.create(
            title="Dimension Test Corpus", creator=self.user
        )

    def test_conversation_search_with_768_dimension(self):
        """Test conversation search with 768-dimensional embeddings."""
        conv = Conversation.objects.create(
            title="768D Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create 768-dimensional embedding
        Embedding.objects.create(
            conversation=conv,
            embedder_path="test/embedder-768",
            vector_768=[0.1] * 768,
            creator=self.user,
        )

        # Search with 768-dimensional vector
        results = Conversation.objects.search_by_embedding(
            query_vector=[0.1] * 768,
            embedder_path="test/embedder-768",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        conv_ids = [c.id for c in results]
        self.assertIn(conv.id, conv_ids)

    def test_conversation_search_with_1536_dimension(self):
        """Test conversation search with 1536-dimensional embeddings."""
        conv = Conversation.objects.create(
            title="1536D Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create 1536-dimensional embedding
        Embedding.objects.create(
            conversation=conv,
            embedder_path="test/embedder-1536",
            vector_1536=[0.1] * 1536,
            creator=self.user,
        )

        # Search with 1536-dimensional vector
        results = Conversation.objects.search_by_embedding(
            query_vector=[0.1] * 1536,
            embedder_path="test/embedder-1536",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        conv_ids = [c.id for c in results]
        self.assertIn(conv.id, conv_ids)

    def test_conversation_search_with_3072_dimension(self):
        """Test conversation search with 3072-dimensional embeddings."""
        conv = Conversation.objects.create(
            title="3072D Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv,
            permissions=[PermissionTypes.ALL],
        )

        # Create 3072-dimensional embedding
        Embedding.objects.create(
            conversation=conv,
            embedder_path="test/embedder-3072",
            vector_3072=[0.1] * 3072,
            creator=self.user,
        )

        # Search with 3072-dimensional vector
        results = Conversation.objects.search_by_embedding(
            query_vector=[0.1] * 3072,
            embedder_path="test/embedder-3072",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        conv_ids = [c.id for c in results]
        self.assertIn(conv.id, conv_ids)

    def test_message_search_with_768_dimension(self):
        """Test message search with 768-dimensional embeddings."""
        conversation = Conversation.objects.create(
            title="768D Message Test",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conversation,
            permissions=[PermissionTypes.ALL],
        )

        message = ChatMessage.objects.create(
            conversation=conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="Test message 768D",
        )

        # Create 768-dimensional embedding
        Embedding.objects.create(
            message=message,
            embedder_path="test/embedder-768",
            vector_768=[0.1] * 768,
            creator=self.user,
        )

        # Search with 768-dimensional vector
        results = ChatMessage.objects.search_by_embedding(
            query_vector=[0.1] * 768,
            embedder_path="test/embedder-768",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        message_ids = [m.id for m in results]
        self.assertIn(message.id, message_ids)

    def test_message_search_with_1536_dimension(self):
        """Test message search with 1536-dimensional embeddings."""
        conversation = Conversation.objects.create(
            title="1536D Message Test",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conversation,
            permissions=[PermissionTypes.ALL],
        )

        message = ChatMessage.objects.create(
            conversation=conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="Test message 1536D",
        )

        # Create 1536-dimensional embedding
        Embedding.objects.create(
            message=message,
            embedder_path="test/embedder-1536",
            vector_1536=[0.1] * 1536,
            creator=self.user,
        )

        # Search with 1536-dimensional vector
        results = ChatMessage.objects.search_by_embedding(
            query_vector=[0.1] * 1536,
            embedder_path="test/embedder-1536",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        message_ids = [m.id for m in results]
        self.assertIn(message.id, message_ids)

    def test_message_search_with_3072_dimension(self):
        """Test message search with 3072-dimensional embeddings."""
        conversation = Conversation.objects.create(
            title="3072D Message Test",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conversation,
            permissions=[PermissionTypes.ALL],
        )

        message = ChatMessage.objects.create(
            conversation=conversation,
            creator=self.user,
            msg_type="HUMAN",
            content="Test message 3072D",
        )

        # Create 3072-dimensional embedding
        Embedding.objects.create(
            message=message,
            embedder_path="test/embedder-3072",
            vector_3072=[0.1] * 3072,
            creator=self.user,
        )

        # Search with 3072-dimensional vector
        results = ChatMessage.objects.search_by_embedding(
            query_vector=[0.1] * 3072,
            embedder_path="test/embedder-3072",
            top_k=10,
        )

        self.assertGreater(len(results), 0)
        message_ids = [m.id for m in results]
        self.assertIn(message.id, message_ids)

    def test_conversation_search_invalid_dimension_raises_error(self):
        """Test that invalid embedding dimensions raise ValueError."""
        with self.assertRaises(ValueError) as ctx:
            Conversation.objects.search_by_embedding(
                query_vector=[0.1] * 512,  # Invalid dimension
                embedder_path="test/embedder",
                top_k=10,
            )

        self.assertIn("Unsupported embedding dimension", str(ctx.exception))

    def test_message_search_invalid_dimension_raises_error(self):
        """Test that invalid message embedding dimensions raise ValueError."""
        with self.assertRaises(ValueError) as ctx:
            ChatMessage.objects.search_by_embedding(
                query_vector=[0.1] * 999,  # Invalid dimension
                embedder_path="test/embedder",
                top_k=10,
            )

        self.assertIn("Unsupported embedding dimension", str(ctx.exception))


class HelperFunctionsTest(TestCase):
    """Test utility helper functions in vector stores module."""

    def test_is_async_context_returns_false_in_sync(self):
        """Test _is_async_context returns False in synchronous context."""
        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _is_async_context,
        )

        # In sync context, should return False
        result = _is_async_context()
        self.assertFalse(result)

    def test_safe_queryset_info_sync_in_sync_context(self):
        """Test _safe_queryset_info_sync in synchronous context."""
        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _safe_queryset_info_sync,
        )

        # Create a simple queryset
        User.objects.create_user(username="helper_user", password="testpass")
        queryset = User.objects.filter(username="helper_user")

        # Should return count info
        result = _safe_queryset_info_sync(queryset, "Test query")
        self.assertIn("Test query:", result)
        self.assertIn("1 results", result)

    def test_safe_queryset_info_sync_handles_exception(self):
        """Test _safe_queryset_info_sync handles queryset exceptions."""
        from unittest.mock import Mock

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _safe_queryset_info_sync,
        )

        # Create a mock queryset that raises an exception
        mock_qs = Mock()
        mock_qs.count.side_effect = Exception("Database error")

        result = _safe_queryset_info_sync(mock_qs, "Error test")
        self.assertIn("unable to count results", result)
        self.assertIn("Database error", result)

    @pytest.mark.asyncio
    async def test_is_async_context_returns_true_in_async(self):
        """Test _is_async_context returns True in asynchronous context."""
        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _is_async_context,
        )

        # In async context, should return True
        result = _is_async_context()
        self.assertTrue(result)

    @pytest.mark.asyncio
    async def test_safe_queryset_info_in_async_context(self):
        """Test _safe_queryset_info in asynchronous context."""
        from asgiref.sync import sync_to_async

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _safe_queryset_info,
        )

        # Create queryset
        await sync_to_async(User.objects.create_user)(
            username="async_info_user", password="testpass"
        )
        queryset = User.objects.filter(username="async_info_user")

        # Should use async path (lines 33-35)
        result = await _safe_queryset_info(queryset, "Async test")
        self.assertIn("Async test:", result)
        self.assertIn("1 results", result)

    @pytest.mark.asyncio
    async def test_safe_queryset_info_handles_exception(self):
        """Test _safe_queryset_info handles exceptions gracefully."""
        from unittest.mock import Mock

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _safe_queryset_info,
        )

        # Create mock queryset that raises exception
        mock_qs = Mock()
        mock_qs.count.side_effect = Exception("Async error")

        result = await _safe_queryset_info(mock_qs, "Error test")
        self.assertIn("unable to count results", result)
        self.assertIn("Async error", result)

    @pytest.mark.asyncio
    async def test_safe_execute_queryset_in_async_context(self):
        """Test _safe_execute_queryset in asynchronous context."""
        from asgiref.sync import sync_to_async

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            _safe_execute_queryset,
        )

        # Create users
        await sync_to_async(User.objects.create_user)(
            username="async_exec1", password="testpass"
        )
        await sync_to_async(User.objects.create_user)(
            username="async_exec2", password="testpass"
        )
        queryset = User.objects.filter(username__startswith="async_exec")

        # Execute in async context (line 56)
        result = await _safe_execute_queryset(queryset)
        self.assertEqual(len(result), 2)


class GraphQLResolverEdgeCasesTest(TestCase):
    """Test edge cases in GraphQL resolver code paths to improve coverage."""

    def setUp(self):
        """Set up test data."""
        self.user = User.objects.create_user(
            username="resolver_edge_user", password="testpassword"
        )
        self.corpus = Corpus.objects.create(
            title="Resolver Edge Case Corpus", creator=self.user
        )
        self.graphene_client = Client(schema, context_value=TestContext(self.user))

        # Create conversation with embedding
        self.conv = Conversation.objects.create(
            title="Edge Case Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=self.conv,
            permissions=[PermissionTypes.ALL],
        )

        Embedding.objects.create(
            conversation=self.conv,
            embedder_path="test/embedder",
            vector_384=[0.1] * 384,
            creator=self.user,
        )

        # Create message with embedding
        self.msg = ChatMessage.objects.create(
            conversation=self.conv,
            creator=self.user,
            msg_type="HUMAN",
            content="Test message content",
        )

        Embedding.objects.create(
            message=self.msg,
            embedder_path="test/embedder",
            vector_384=[0.2] * 384,
            creator=self.user,
        )

    def test_search_conversations_without_corpus_or_document_id(self):
        """Test searchConversations without corpus_id or document_id (requires DEFAULT_EMBEDDER_PATH)."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            ConversationSearchResult,
        )

        query = """
            query SearchConversations($query: String!) {
                searchConversations(query: $query) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        # Mock the vector store to avoid embedder dependency
        mock_result = ConversationSearchResult(
            conversation=self.conv,
            similarity_score=0.95,
        )

        mock_store = Mock()
        mock_store.search.return_value = [mock_result]

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreConversationVectorStore",
            return_value=mock_store,
        ):
            # Mock settings to provide DEFAULT_EMBEDDER_PATH
            with override_settings(DEFAULT_EMBEDDER_PATH="default/embedder"):
                result = self.graphene_client.execute(
                    query,
                    variables={"query": "test query without corpus"},
                )

                # Should succeed with mocked embedder path
                self.assertIsNone(result.get("errors"))
                self.assertIsNotNone(result.get("data"))

    def test_search_conversations_without_embedder_path_raises_error(self):
        """Test searchConversations without corpus_id/document_id and no DEFAULT_EMBEDDER_PATH."""
        query = """
            query SearchConversations($query: String!) {
                searchConversations(query: $query) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }
        """

        # Mock settings with no DEFAULT_EMBEDDER_PATH
        with override_settings(DEFAULT_EMBEDDER_PATH=None):
            result = self.graphene_client.execute(
                query,
                variables={"query": "test query"},
            )

            # Should have error about missing embedder path
            self.assertIsNotNone(result.get("errors"))
            error_message = result["errors"][0]["message"]
            self.assertIn("DEFAULT_EMBEDDER_PATH", error_message)

    def test_search_messages_without_corpus_or_conversation_id(self):
        """Test searchMessages without corpus_id or conversation_id (requires DEFAULT_EMBEDDER_PATH)."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            MessageSearchResult,
        )

        query = """
            query SearchMessages($query: String!) {
                searchMessages(query: $query) {
                    id
                    content
                }
            }
        """

        # Mock the vector store
        mock_result = MessageSearchResult(
            message=self.msg,
            similarity_score=0.95,
        )

        mock_store = Mock()
        mock_store.search.return_value = [mock_result]

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreChatMessageVectorStore",
            return_value=mock_store,
        ):
            # Mock settings to provide DEFAULT_EMBEDDER_PATH
            with override_settings(DEFAULT_EMBEDDER_PATH="default/embedder"):
                result = self.graphene_client.execute(
                    query,
                    variables={"query": "test message query"},
                )

                # Should succeed with mocked embedder path
                self.assertIsNone(result.get("errors"))
                self.assertIsNotNone(result.get("data"))
                messages = result["data"]["searchMessages"]
                self.assertEqual(len(messages), 1)

    def test_search_messages_without_embedder_path_raises_error(self):
        """Test searchMessages without corpus_id/conversation_id and no DEFAULT_EMBEDDER_PATH."""
        query = """
            query SearchMessages($query: String!) {
                searchMessages(query: $query) {
                    id
                    content
                }
            }
        """

        # Mock settings with no DEFAULT_EMBEDDER_PATH
        with override_settings(DEFAULT_EMBEDDER_PATH=None):
            result = self.graphene_client.execute(
                query,
                variables={"query": "test message query"},
            )

            # Should have error about missing embedder path
            self.assertIsNotNone(result.get("errors"))
            error_message = result["errors"][0]["message"]
            self.assertIn("DEFAULT_EMBEDDER_PATH", error_message)

    def test_search_conversations_with_last_and_before_pagination(self):
        """Test searchConversations with last and before parameters for reverse pagination."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            ConversationSearchResult,
        )

        query = """
            query SearchConversations($query: String!, $last: Int, $before: String) {
                searchConversations(query: $query, last: $last, before: $before) {
                    edges {
                        node {
                            id
                            title
                        }
                        cursor
                    }
                    pageInfo {
                        hasPreviousPage
                        startCursor
                    }
                }
            }
        """

        # Create multiple mock results
        mock_results = [
            ConversationSearchResult(conversation=self.conv, similarity_score=0.95)
        ]

        mock_store = Mock()
        mock_store.search.return_value = mock_results

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreConversationVectorStore",
            return_value=mock_store,
        ):
            result = self.graphene_client.execute(
                query,
                variables={
                    "query": "test query",
                    "last": 5,
                    "before": "cursor_value",
                },
            )

            # Should succeed (cursor will be handled by relay)
            # May have errors if cursor is invalid, but should hit the code path
            if not result.get("errors"):
                self.assertIsNotNone(result.get("data"))

    def test_search_conversations_returns_multiple_results(self):
        """Test searchConversations returns multiple results and pagination works correctly."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            ConversationSearchResult,
        )

        # Create multiple conversations
        conv2 = Conversation.objects.create(
            title="Second Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv2,
            permissions=[PermissionTypes.ALL],
        )

        conv3 = Conversation.objects.create(
            title="Third Conversation",
            chat_with_corpus=self.corpus,
            creator=self.user,
        )
        set_permissions_for_obj_to_user(
            user_val=self.user,
            instance=conv3,
            permissions=[PermissionTypes.ALL],
        )

        query = """
            query SearchConversations($query: String!, $first: Int) {
                searchConversations(query: $query, first: $first) {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                    pageInfo {
                        hasNextPage
                    }
                    totalCount
                }
            }
        """

        # Create mock results for all conversations
        mock_results = [
            ConversationSearchResult(conversation=self.conv, similarity_score=0.95),
            ConversationSearchResult(conversation=conv2, similarity_score=0.90),
            ConversationSearchResult(conversation=conv3, similarity_score=0.85),
        ]

        mock_store = Mock()
        mock_store.search.return_value = mock_results

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreConversationVectorStore",
            return_value=mock_store,
        ):
            # Need DEFAULT_EMBEDDER_PATH since no corpus_id/document_id provided
            with override_settings(DEFAULT_EMBEDDER_PATH="default/embedder"):
                result = self.graphene_client.execute(
                    query,
                    variables={
                        "query": "test query",
                        "first": 2,
                    },
                )

                # Should succeed and return paginated results
                self.assertIsNone(result.get("errors"))
                self.assertIsNotNone(result.get("data"))

                search_result = result["data"]["searchConversations"]
                self.assertEqual(search_result["totalCount"], 3)
                self.assertEqual(len(search_result["edges"]), 2)
                self.assertTrue(search_result["pageInfo"]["hasNextPage"])

    def test_search_messages_returns_multiple_results(self):
        """Test searchMessages returns multiple results correctly."""
        from unittest.mock import Mock, patch

        from opencontractserver.llms.vector_stores.core_conversation_vector_stores import (
            MessageSearchResult,
        )

        # Create additional messages
        msg2 = ChatMessage.objects.create(
            conversation=self.conv,
            creator=self.user,
            msg_type="LLM",
            content="Second message",
        )

        msg3 = ChatMessage.objects.create(
            conversation=self.conv,
            creator=self.user,
            msg_type="HUMAN",
            content="Third message",
        )

        query = """
            query SearchMessages($query: String!, $topK: Int) {
                searchMessages(query: $query, topK: $topK) {
                    id
                    content
                    msgType
                }
            }
        """

        # Create mock results
        mock_results = [
            MessageSearchResult(message=self.msg, similarity_score=0.95),
            MessageSearchResult(message=msg2, similarity_score=0.90),
            MessageSearchResult(message=msg3, similarity_score=0.85),
        ]

        mock_store = Mock()
        mock_store.search.return_value = mock_results

        with patch(
            "opencontractserver.llms.vector_stores.core_conversation_vector_stores.CoreChatMessageVectorStore",
            return_value=mock_store,
        ):
            # Need DEFAULT_EMBEDDER_PATH since no corpus_id/conversation_id provided
            with override_settings(DEFAULT_EMBEDDER_PATH="default/embedder"):
                result = self.graphene_client.execute(
                    query,
                    variables={
                        "query": "test message query",
                        "topK": 10,
                    },
                )

                # Should succeed and return all messages
                self.assertIsNone(result.get("errors"))
                self.assertIsNotNone(result.get("data"))

                messages = result["data"]["searchMessages"]
                self.assertEqual(len(messages), 3)

                # Verify messages are returned
                msg_ids = [
                    to_global_id("MessageType", m.id) for m in [self.msg, msg2, msg3]
                ]
                returned_ids = [m["id"] for m in messages]
                for msg_id in msg_ids:
                    self.assertIn(msg_id, returned_ids)
