"""Integration tests for Databricks Genie tool functionality."""

from __future__ import annotations

import os
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from unittest.mock import Mock, patch

import pandas as pd
import pytest
from conftest import add_databricks_resource_attrs, has_retail_ai_env
from databricks.sdk.service.sql import StatementState
from databricks_ai_bridge.genie import Genie, GenieResponse
from langchain_core.tools import StructuredTool

if TYPE_CHECKING:
    from databricks.sdk import WorkspaceClient

from dao_ai.config import (
    GenieContextAwareCacheParametersModel,
    GenieLRUCacheParametersModel,
    GenieRoomModel,
)
from dao_ai.genie import GenieFeedbackRating, GenieServiceBase
from dao_ai.genie.cache import (
    CacheResult,
    LRUCacheService,
    PostgresContextAwareGenieService,
    SQLCacheEntry,
)
from dao_ai.tools.genie import create_genie_tool


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_tool_real_api_integration() -> None:
    """
    Real integration test that invokes the actual Genie service without mocks.

    This test requires:
    - Valid DATABRICKS_HOST and DATABRICKS_TOKEN environment variables
    - Access to the configured Genie space
    - Proper permissions to query the Genie service

    This test will make real API calls to Databricks.
    Note: The Genie tool requires InjectedState and InjectedToolCallId, so we test
    the underlying Genie class directly for real API integration.
    """
    # Use the real space ID from the retail AI environment
    real_space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")

    try:
        # Create a real Genie instance directly (bypasses tool framework dependencies)
        print(f"\nCreating real Genie instance for space: {real_space_id}")
        genie = Genie(space_id=real_space_id)

        # Verify Genie instance was created successfully
        assert genie.space_id == real_space_id
        assert genie.headers["Accept"] == "application/json"
        print("Genie instance created successfully")
        if genie.description:
            print(f"Space description: {genie.description[:100]}...")
        else:
            print("Space description: None")

        # Test 1: Ask a simple question to start a new conversation
        print("\nTesting real Genie API - Question 1...")
        question1 = "How many tables are available in this space?"
        result1 = genie.ask_question(question1, conversation_id=None)

        # Verify we got a valid response
        assert isinstance(result1, GenieResponse)
        assert result1.conversation_id is not None
        assert len(result1.conversation_id) > 0
        assert result1.result is not None

        # Store the conversation ID for follow-up
        conversation_id = result1.conversation_id
        print(f"First question successful, conversation_id: {conversation_id}")
        print(f"Result: {str(result1.result)[:100]}...")  # Show first 100 chars
        if result1.query:
            print(f"Query: {result1.query}")

        # Test 2: Ask a follow-up question using the same conversation
        print("\nTesting conversation persistence - Question 2...")
        question2 = "Can you show me the schema of the first table?"
        result2 = genie.ask_question(question2, conversation_id=conversation_id)

        # Verify follow-up response
        assert isinstance(result2, GenieResponse)
        assert (
            result2.conversation_id == conversation_id
        )  # Should maintain same conversation
        assert result2.result is not None

        print(
            f"Follow-up question successful, same conversation_id: {result2.conversation_id}"
        )
        print(f"Result: {str(result2.result)[:100]}...")  # Show first 100 chars
        if result2.query:
            print(f"Query: {result2.query}")

        # Test 3: Start a completely new conversation
        print("\nTesting new conversation creation - Question 3...")
        question3 = "What is the total number of records across all tables?"
        result3 = genie.ask_question(question3, conversation_id=None)

        # Verify new conversation was created
        assert isinstance(result3, GenieResponse)
        assert result3.conversation_id is not None
        assert (
            result3.conversation_id != conversation_id
        )  # Should be different conversation
        assert result3.result is not None

        print(
            f"New conversation successful, new conversation_id: {result3.conversation_id}"
        )
        print(f"Result: {str(result3.result)[:100]}...")  # Show first 100 chars
        if result3.query:
            print(f"Query: {result3.query}")

        # Test 4: Continue the second conversation
        print("\nTesting second conversation continuation - Question 4...")
        question4 = "Can you break that down by table?"
        result4 = genie.ask_question(question4, conversation_id=result3.conversation_id)

        # Verify second conversation continuation
        assert isinstance(result4, GenieResponse)
        assert (
            result4.conversation_id == result3.conversation_id
        )  # Should maintain same conversation
        assert result4.result is not None

        print(
            f"Second conversation continued, conversation_id: {result4.conversation_id}"
        )
        print(f"Result: {str(result4.result)[:100]}...")  # Show first 100 chars
        if result4.query:
            print(f"Query: {result4.query}")

        # Summary
        print("\nReal API Integration Test Summary:")
        print(
            f"   - Question 1 (new conv): conversation_id = {result1.conversation_id}"
        )
        print(
            f"   - Question 2 (continue conv 1): conversation_id = {result2.conversation_id}"
        )
        print(
            f"   - Question 3 (new conv): conversation_id = {result3.conversation_id}"
        )
        print(
            f"   - Question 4 (continue conv 2): conversation_id = {result4.conversation_id}"
        )
        print(
            f"   - Conv 1 persistence: {'PASS' if result1.conversation_id == result2.conversation_id else 'FAIL'}"
        )
        print(
            f"   - Conv 2 persistence: {'PASS' if result3.conversation_id == result4.conversation_id else 'FAIL'}"
        )
        print(
            f"   - Conv isolation: {'PASS' if result1.conversation_id != result3.conversation_id else 'FAIL'}"
        )

    except Exception as e:
        # Provide helpful error information for debugging
        print("\nReal API integration test failed:")
        print(f"   Error type: {type(e).__name__}")
        print(f"   Error message: {str(e)}")

        # Check for common issues
        if "PermissionDenied" in str(e):
            print("   Permission issue - check DATABRICKS_TOKEN and space access")
        elif "NotFound" in str(e):
            print(f"   Space not found - check space_id: {real_space_id}")
        elif "NetworkError" in str(e) or "ConnectionError" in str(e):
            print("   Network issue - check DATABRICKS_HOST and connectivity")

        # Re-raise to fail the test
        raise


@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_create_genie_tool_parameters() -> None:
    """Test creating a genie tool with both default and custom parameters."""
    # Test 1: Default parameters
    genie_room_default = GenieRoomModel(
        name="Minimal Test Room",
        description="Minimal configuration test",
        space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
    )

    # Create tool with defaults (no name or description override)
    tool_default = create_genie_tool(genie_room=genie_room_default)

    # Verify defaults were applied
    assert isinstance(tool_default, StructuredTool)
    assert tool_default.name == "genie_tool"  # Default name from function
    assert (
        "This tool lets you have a conversation and chat with tabular data"
        in tool_default.description
    )
    assert "question" in tool_default.args_schema.model_fields
    assert "ask simple clear questions" in tool_default.description
    assert (
        "multiple times rather than asking a complex question"
        in tool_default.description
    )

    # Test 2: Custom parameters
    genie_room_custom = GenieRoomModel(
        name="Custom Test Room",
        description="Custom configuration test",
        space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
    )

    custom_name = "my_custom_genie_tool"
    custom_description = "This is my custom genie tool for testing retail data queries."

    tool_custom = create_genie_tool(
        genie_room=genie_room_custom, name=custom_name, description=custom_description
    )

    # Verify custom parameters were applied
    assert isinstance(tool_custom, StructuredTool)
    assert tool_custom.name == custom_name
    assert custom_description in tool_custom.description
    assert "question" in tool_custom.args_schema.model_fields
    assert "Args:" in tool_custom.description
    assert "question (str): The question to ask to ask Genie" in tool_custom.description
    assert "Returns:" in tool_custom.description
    assert "GenieResponse" in tool_custom.description


@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_tool_error_handling() -> None:
    """Test genie tool handles errors gracefully."""
    # Create genie room configuration
    genie_room = GenieRoomModel(
        name="Error Test Room",
        description="Test error handling",
        space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
    )

    # Create the genie tool
    tool = create_genie_tool(genie_room=genie_room, name="error_test_tool")

    # Verify tool structure
    assert isinstance(tool, StructuredTool)
    assert tool.name == "error_test_tool"

    # Test error handling at the Genie class level
    with patch.object(Genie, "ask_question") as mock_ask:
        # Simulate an error response
        mock_error_response = GenieResponse(
            conversation_id="conv_error",
            result="Genie query failed with error: Invalid SQL syntax",
            query="SELECT * FROM non_existent_table",
            description="Failed query",
        )
        mock_ask.return_value = mock_error_response

        # Create Genie instance and test error handling
        genie = Genie(space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"))
        result = genie.ask_question("SELECT * FROM non_existent_table")

        # Verify error was handled gracefully
        mock_ask.assert_called_once()
        assert result.conversation_id == "conv_error"
        assert "Genie query failed with error" in result.result
        assert result.query == "SELECT * FROM non_existent_table"


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_api_conversation_flow() -> None:
    """Integration test for Genie API conversation flow with mocked responses."""
    # Create genie room configuration with real space ID
    genie_room = GenieRoomModel(
        name="API Flow Test Room",
        description="Test API conversation flow",
        space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
    )

    # Create the genie tool
    tool = create_genie_tool(
        genie_room=genie_room,
        name="api_flow_test_tool",
        description="API flow test tool",
    )

    # Verify tool structure
    assert isinstance(tool, StructuredTool)
    assert tool.name == "api_flow_test_tool"
    assert "question" in tool.args_schema.model_fields

    # Test the conversation flow logic with detailed mocking
    with (
        patch.object(Genie, "start_conversation") as mock_start,
        patch.object(Genie, "create_message") as mock_create,
        patch.object(Genie, "poll_for_result") as mock_poll,
    ):
        # Mock responses for conversation flow
        mock_start.return_value = {
            "conversation_id": "flow_conv_789",
            "message_id": "msg_123",
        }

        mock_create.return_value = {
            "conversation_id": "flow_conv_789",
            "message_id": "msg_456",
        }

        mock_poll_result = GenieResponse(
            conversation_id="flow_conv_789",
            result="Flow test result",
            query="SELECT count(*) FROM test_table",
            description="Count query",
        )
        mock_poll.return_value = mock_poll_result

        # Create Genie instance and test flow
        genie = Genie(space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"))

        # Test first question (new conversation)
        result1 = genie.ask_question(
            "How many records are there?", conversation_id=None
        )

        # Verify start_conversation was called for new conversation
        mock_start.assert_called_once_with("How many records are there?")
        mock_poll.assert_called_once_with("flow_conv_789", "msg_123")
        assert result1.conversation_id == "flow_conv_789"

        # Reset mocks for second call
        mock_start.reset_mock()
        mock_poll.reset_mock()

        # Test follow-up question (existing conversation)
        result2 = genie.ask_question(
            "Show me the data", conversation_id="flow_conv_789"
        )

        # Verify create_message was called for existing conversation
        mock_create.assert_called_once_with("flow_conv_789", "Show me the data")
        mock_poll.assert_called_once_with("flow_conv_789", "msg_456")
        mock_start.assert_not_called()  # Should not start new conversation
        assert result2.conversation_id == "flow_conv_789"


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_real_api_conversation_reuse_example() -> None:
    """
    Example test showing how to use the real Genie API with conversation ID reuse.

    This test demonstrates the proper pattern for maintaining conversation context
    across multiple questions, which is the core functionality needed for agents.
    """
    real_space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")

    print("\n" + "=" * 50)
    print("GENIE API CONVERSATION REUSE EXAMPLE")
    print("=" * 50)

    try:
        # Step 1: Initialize Genie client
        print("\n1. Initializing Genie client...")
        genie = Genie(space_id=real_space_id)
        print(f"   Connected to space: {real_space_id}")

        # Step 2: Start first conversation with a broad question
        print("\n2. Starting new conversation with initial question...")
        question_1 = "What tables are available in this data space?"
        print(f"   Question: {question_1}")

        result_1 = genie.ask_question(question_1, conversation_id=None)
        conversation_id = result_1.conversation_id

        print(f"   ✓ New conversation created: {conversation_id}")
        print(f"   ✓ Response: {str(result_1.result)[:150]}...")
        if result_1.query:
            print(f"   ✓ Generated SQL: {result_1.query}")

        # Step 3: Continue same conversation with follow-up question
        print(f"\n3. Continuing conversation {conversation_id}...")
        question_2 = "Show me the first few rows from the largest table"
        print(f"   Question: {question_2}")

        result_2 = genie.ask_question(question_2, conversation_id=conversation_id)

        print(f"   ✓ Same conversation continued: {result_2.conversation_id}")
        print(f"   ✓ Response: {str(result_2.result)[:150]}...")
        if result_2.query:
            print(f"   ✓ Generated SQL: {result_2.query}")

        # Step 4: Ask related follow-up in same conversation
        print(f"\n4. Another follow-up in conversation {conversation_id}...")
        question_3 = "How many total records are in that table?"
        print(f"   Question: {question_3}")

        result_3 = genie.ask_question(question_3, conversation_id=conversation_id)

        print(f"   ✓ Conversation maintained: {result_3.conversation_id}")
        print(f"   ✓ Response: {str(result_3.result)[:150]}...")
        if result_3.query:
            print(f"   ✓ Generated SQL: {result_3.query}")

        # Step 5: Start completely new conversation
        print("\n5. Starting new conversation (different topic)...")
        question_4 = "What are the column names and data types for all tables?"
        print(f"   Question: {question_4}")

        result_4 = genie.ask_question(question_4, conversation_id=None)
        new_conversation_id = result_4.conversation_id

        print(f"   ✓ New conversation started: {new_conversation_id}")
        print(f"   ✓ Response: {str(result_4.result)[:150]}...")
        if result_4.query:
            print(f"   ✓ Generated SQL: {result_4.query}")

        # Validation
        print("\n6. Validation Results:")
        print(f"   ✓ First conversation: {conversation_id}")
        print(f"   ✓ Second conversation: {new_conversation_id}")
        print(
            f"   ✓ Conversation persistence: {'PASS' if result_1.conversation_id == result_2.conversation_id == result_3.conversation_id else 'FAIL'}"
        )
        print(
            f"   ✓ Conversation isolation: {'PASS' if conversation_id != new_conversation_id else 'FAIL'}"
        )

        # Assert validation
        assert (
            result_1.conversation_id
            == result_2.conversation_id
            == result_3.conversation_id
        )
        assert conversation_id != new_conversation_id
        assert all(
            r.result is not None for r in [result_1, result_2, result_3, result_4]
        )

        print("\n✓ All tests passed! Conversation reuse working correctly.")

    except Exception as e:
        print(f"\n✗ Test failed: {type(e).__name__}: {str(e)}")
        raise


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_tool_usage_pattern_with_state() -> None:
    """
    Test showing how the Genie tool would be used in a real LangGraph application.

    This demonstrates the proper usage pattern with AgentState for conversation
    persistence, which is how agents would actually use this tool.
    """
    from dao_ai.state import AgentState

    real_space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")

    print("\n" + "=" * 50)
    print("GENIE TOOL USAGE PATTERN WITH STATE")
    print("=" * 50)

    # Create the tool as it would be in a real application
    genie_room = GenieRoomModel(
        name="State Test Room",
        description="Test tool usage with state management",
        space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
    )

    tool = create_genie_tool(
        genie_room=genie_room,
        name="state_test_genie_tool",
        description="Genie tool for state-based conversation testing",
    )

    print(f"\n1. Created tool: {tool.name}")
    print(f"   Description: {tool.description[:100]}...")

    # Simulate how the tool would be called in LangGraph with state
    print("\n2. Simulating LangGraph usage pattern...")

    # Mock the tool function to demonstrate the calling pattern
    # In real usage, LangGraph would inject the state and tool_call_id
    with patch.object(Genie, "ask_question") as mock_ask:
        # Setup mock responses
        mock_responses = [
            GenieResponse(
                conversation_id="state_conv_123",
                result="Found 5 tables: customers, orders, products, inventory, sales",
                query="SHOW TABLES",
                description="Table listing query",
            ),
            GenieResponse(
                conversation_id="state_conv_123",
                result="customers table has 10,000 rows with columns: id, name, email, created_at",
                query="DESCRIBE customers",
                description="Table description query",
            ),
            GenieResponse(
                conversation_id="state_conv_123",
                result="Sample data: [{'id': 1, 'name': 'John Doe', 'email': 'john@example.com'}]",
                query="SELECT * FROM customers LIMIT 3",
                description="Sample data query",
            ),
        ]

        mock_ask.side_effect = mock_responses

        # Simulate state management as LangGraph would do it
        shared_state = AgentState()

        # Simulate first tool call (no existing conversation)
        print("\n3. First question (new conversation)...")
        question1 = "What tables are available?"
        print(f"   Question: {question1}")

        # This is how the tool function would be called internally
        # (we can't call tool.invoke directly due to InjectedState/InjectedToolCallId)
        genie = Genie(space_id=real_space_id)

        # Simulate getting conversation_id from state mapping (initially None)
        space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")
        conversation_ids = shared_state.get("genie_conversation_ids", {})
        existing_conversation_id = conversation_ids.get(space_id)
        print(
            f"   Existing conversation_id for space {space_id}: {existing_conversation_id}"
        )

        result1 = genie.ask_question(
            question1, conversation_id=existing_conversation_id
        )

        # Simulate updating state with new conversation_id
        updated_conversation_ids = conversation_ids.copy()
        updated_conversation_ids[space_id] = result1.conversation_id
        shared_state.update({"genie_conversation_ids": updated_conversation_ids})
        print(
            f"   ✓ New conversation_id saved to state for space {space_id}: {result1.conversation_id}"
        )
        print(f"   ✓ Response: {result1.result}")

        # Simulate second tool call (reusing conversation)
        print("\n4. Second question (reusing conversation)...")
        question2 = "Tell me more about the customers table"
        print(f"   Question: {question2}")

        # Get conversation_id from state mapping
        conversation_ids = shared_state.get("genie_conversation_ids", {})
        existing_conversation_id = conversation_ids.get(space_id)
        print(
            f"   Retrieved conversation_id for space {space_id}: {existing_conversation_id}"
        )

        result2 = genie.ask_question(
            question2, conversation_id=existing_conversation_id
        )

        # Update state (conversation_id should be the same)
        updated_conversation_ids = conversation_ids.copy()
        updated_conversation_ids[space_id] = result2.conversation_id
        shared_state.update({"genie_conversation_ids": updated_conversation_ids})
        print(f"   ✓ Conversation_id maintained: {result2.conversation_id}")
        print(f"   ✓ Response: {result2.result}")

        # Simulate third tool call (continuing conversation)
        print("\n5. Third question (continuing conversation)...")
        question3 = "Show me some sample data from that table"
        print(f"   Question: {question3}")

        # Get conversation_id from state mapping
        conversation_ids = shared_state.get("genie_conversation_ids", {})
        existing_conversation_id = conversation_ids.get(space_id)
        result3 = genie.ask_question(
            question3, conversation_id=existing_conversation_id
        )

        # Update state mapping
        updated_conversation_ids = conversation_ids.copy()
        updated_conversation_ids[space_id] = result3.conversation_id
        shared_state.update({"genie_conversation_ids": updated_conversation_ids})
        print(f"   ✓ Conversation continues: {result3.conversation_id}")
        print(f"   ✓ Response: {result3.result}")

        # Validation
        print("\n6. State Management Validation:")
        final_conversation_ids = shared_state.get("genie_conversation_ids", {})
        final_conversation_id = final_conversation_ids.get(space_id)
        print(
            f"   ✓ Final conversation_id for space {space_id}: {final_conversation_id}"
        )
        print(
            f"   ✓ All responses used same conversation: {'PASS' if result1.conversation_id == result2.conversation_id == result3.conversation_id else 'FAIL'}"
        )
        print(
            f"   ✓ State properly maintained conversation: {'PASS' if final_conversation_id == result1.conversation_id else 'FAIL'}"
        )

        # Verify the mock calls
        assert mock_ask.call_count == 3

        # Check that first call had no conversation_id
        first_call = mock_ask.call_args_list[0]
        assert first_call.kwargs["conversation_id"] is None

        # Check that subsequent calls used the same conversation_id
        second_call = mock_ask.call_args_list[1]
        third_call = mock_ask.call_args_list[2]
        assert second_call.kwargs["conversation_id"] == "state_conv_123"
        assert third_call.kwargs["conversation_id"] == "state_conv_123"

        # Verify all responses have same conversation_id
        assert (
            result1.conversation_id
            == result2.conversation_id
            == result3.conversation_id
        )

        print("\n✓ State-based conversation management working correctly!")


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_genie_conversation_lifecycle_example() -> None:
    """
    Complete example showing the full lifecycle of Genie conversations.

    This demonstrates how conversations are created, maintained, and isolated
    in a realistic usage scenario.

    Note: This test is marked as flaky due to intermittent StopIteration errors
    from the external databricks_ai_bridge library's poll_result method.
    """
    real_space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")

    print("\n" + "=" * 60)
    print("COMPLETE GENIE CONVERSATION LIFECYCLE EXAMPLE")
    print("=" * 60)

    try:
        genie = Genie(space_id=real_space_id)
        print(f"Initialized Genie client for space: {real_space_id}")

        # === SCENARIO 1: Data Exploration Conversation ===
        print("\n📈 SCENARIO 1: Data Exploration")
        print("-" * 40)

        # Start exploration conversation
        exploration_q1 = "What data do we have available? Show me all tables."
        print(f"Q1: {exploration_q1}")
        result1 = genie.ask_question(exploration_q1, conversation_id=None)
        exploration_conv_id = result1.conversation_id

        print(f"   → Conversation started: {exploration_conv_id}")
        print(f"   → Result: {str(result1.result)[:100]}...")

        # Continue exploration in same conversation
        exploration_q2 = "What's the schema of the largest table?"
        print(f"Q2: {exploration_q2}")
        result2 = genie.ask_question(
            exploration_q2, conversation_id=exploration_conv_id
        )

        print(f"   → Conversation continued: {result2.conversation_id}")
        print(f"   → Result: {str(result2.result)[:100]}...")

        # More exploration
        exploration_q3 = "Show me a sample of 5 rows from that table"
        print(f"Q3: {exploration_q3}")
        result3 = genie.ask_question(
            exploration_q3, conversation_id=exploration_conv_id
        )

        print(f"   → Conversation continued: {result3.conversation_id}")
        print(f"   → Result: {str(result3.result)[:100]}...")

        # === SCENARIO 2: Business Analytics Conversation ===
        print("\n📊 SCENARIO 2: Business Analytics (New Topic)")
        print("-" * 40)

        # Start new conversation for different topic
        analytics_q1 = "What are the key metrics I can calculate from this data?"
        print(f"Q1: {analytics_q1}")
        result4 = genie.ask_question(analytics_q1, conversation_id=None)
        analytics_conv_id = result4.conversation_id

        print(f"   → New conversation started: {analytics_conv_id}")
        print(f"   → Result: {str(result4.result)[:100]}...")

        # Continue analytics conversation
        analytics_q2 = "Calculate the total revenue for the last month"
        print(f"Q2: {analytics_q2}")
        result5 = genie.ask_question(analytics_q2, conversation_id=analytics_conv_id)

        print(f"   → Analytics conversation continued: {result5.conversation_id}")
        print(f"   → Result: {str(result5.result)[:100]}...")

        # === SCENARIO 3: Return to Exploration ===
        print("\n🔄 SCENARIO 3: Return to Data Exploration")
        print("-" * 40)

        # Return to original exploration conversation
        exploration_q4 = (
            "Based on what we saw earlier, are there any data quality issues?"
        )
        print(f"Q4: {exploration_q4}")
        result6 = genie.ask_question(
            exploration_q4, conversation_id=exploration_conv_id
        )

        print(f"   → Back to exploration conversation: {result6.conversation_id}")
        print(f"   → Result: {str(result6.result)[:100]}...")

        # === VALIDATION AND SUMMARY ===
        print("\n✅ CONVERSATION LIFECYCLE SUMMARY")
        print("-" * 40)

        print(f"Exploration Conversation: {exploration_conv_id}")
        print(
            f"  - Questions 1, 2, 3, 4: {[r.conversation_id for r in [result1, result2, result3, result6]]}"
        )
        print(
            f"  - All same conversation: {'✓' if all(r.conversation_id == exploration_conv_id for r in [result1, result2, result3, result6]) else '✗'}"
        )

        print(f"\nAnalytics Conversation: {analytics_conv_id}")
        print(f"  - Questions 1, 2: {[r.conversation_id for r in [result4, result5]]}")
        print(
            f"  - All same conversation: {'✓' if all(r.conversation_id == analytics_conv_id for r in [result4, result5]) else '✗'}"
        )

        print("\nConversation Isolation:")
        print(
            f"  - Different conversation IDs: {'✓' if exploration_conv_id != analytics_conv_id else '✗'}"
        )
        print(
            f"  - Context maintained separately: {'✓' if len(set([exploration_conv_id, analytics_conv_id])) == 2 else '✗'}"
        )

        # Assert all validations
        assert all(
            r.conversation_id == exploration_conv_id
            for r in [result1, result2, result3, result6]
        )
        assert all(r.conversation_id == analytics_conv_id for r in [result4, result5])
        assert exploration_conv_id != analytics_conv_id
        assert all(
            r.result is not None
            for r in [result1, result2, result3, result4, result5, result6]
        )

        print("\n🎉 Complete conversation lifecycle test PASSED!")
        print("   • Multiple conversations maintained independently")
        print("   • Context preserved within each conversation")
        print("   • Conversations can be resumed after switching topics")

    except Exception as e:
        print(f"\n❌ Lifecycle test FAILED: {type(e).__name__}: {str(e)}")
        raise


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_with_app_config_and_responses_agent() -> None:
    """
    Integration test that loads the genie.yaml config file, creates a ResponsesAgent,
    and invokes the genie tool through the agent framework.

    This test demonstrates the complete end-to-end flow from YAML configuration
    to agent execution with the Genie tool.
    """
    from mlflow.types.responses import ResponsesAgentRequest
    from mlflow.types.responses_helpers import Message, ResponseInputTextParam

    from dao_ai.config import AppConfig

    print("\n" + "=" * 60)
    print("GENIE APP CONFIG AND RESPONSES AGENT INTEGRATION TEST")
    print("=" * 60)

    try:
        # Step 1: Load configuration from YAML file
        config_path = "config/examples/04_genie/genie_basic.yaml"
        print(f"\n1. Loading configuration from: {config_path}")

        app_config = AppConfig.from_file(config_path)
        print("   ✓ Configuration loaded successfully")
        print(f"   ✓ App name: {app_config.app.name}")
        print(f"   ✓ App description: {app_config.app.description}")
        print(f"   ✓ Number of agents: {len(app_config.app.agents)}")

        # Step 2: Create ResponsesAgent from config
        print("\n2. Creating ResponsesAgent from configuration...")
        responses_agent = app_config.as_responses_agent()
        print("   ✓ ResponsesAgent created successfully")
        print(f"   ✓ Agent type: {type(responses_agent).__name__}")

        # Step 3: Prepare request to test the genie tool
        print("\n3. Preparing request to invoke genie tool...")

        # Create a request that should trigger the genie tool
        question = "What tables are available in this data space?"
        print(f"   Question: {question}")

        request = ResponsesAgentRequest(
            input=[
                Message(
                    role="user",
                    content=[ResponseInputTextParam(type="text", text=question)],
                )
            ]
        )

        print(f"   ✓ Request prepared with {len(request.input)} message(s)")

        # Step 4: Invoke the agent (which should use the genie tool)
        print("\n4. Invoking ResponsesAgent...")

        response = responses_agent.predict(request)

        print("   ✓ Agent invocation completed")
        print(f"   ✓ Response type: {type(response).__name__}")

        # Step 5: Validate response
        print("\n5. Validating response...")

        assert response is not None, "Response should not be None"
        assert hasattr(response, "output"), "Response should have output"
        assert len(response.output) > 0, "Response should have at least one output item"

        output_item = response.output[0]
        assert hasattr(output_item, "content"), "Output item should have content"
        assert len(output_item.content) > 0, "Output item should have content items"

        # Extract text content from the output
        response_content = ""
        for content_item in output_item.content:
            if (
                isinstance(content_item, dict)
                and content_item.get("type") == "output_text"
            ):
                response_content += content_item.get("text", "")

        print(f"   ✓ Response content length: {len(response_content)} characters")
        print(f"   ✓ Response preview: {response_content[:200]}...")

        # Step 6: Verify the response contains data-related content or shows tool was invoked
        print("\n6. Verifying genie tool was invoked...")

        # The response should contain information about tables, data, or indicate the tool was used
        response_lower = response_content.lower()
        data_indicators = [
            "table",
            "data",
            "schema",
            "database",
            "sql",
            "query",
            "column",
            "genie",
            "tool",
            "technical issue",
        ]

        found_indicators = [
            indicator for indicator in data_indicators if indicator in response_lower
        ]
        print(f"   ✓ Found relevant terms: {found_indicators}")

        # Assert that we found at least one relevant term (including error messages indicating tool was called)
        assert len(found_indicators) > 0, (
            f"Response should contain relevant terms, but got: {response_content[:500]}..."
        )

        print("\n7. Integration Test Summary:")
        print("   ✓ Configuration loaded from YAML: ✓")
        print("   ✓ ResponsesAgent created: ✓")
        print("   ✓ Agent invoked successfully: ✓")
        print("   ✓ Genie tool appears to have been used: ✓")
        print("   ✓ Response contains data-related content: ✓")

        print("\n🎉 Complete end-to-end integration test PASSED!")
        print("   • YAML config → AppConfig → ResponsesAgent → Genie Tool → Response")
        print("   • Configuration-driven agent successfully answered data question")

    except Exception as e:
        print(f"\n❌ Integration test FAILED: {type(e).__name__}: {str(e)}")
        print(f"   Error details: {str(e)}")
        raise


@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_genie_config_validation_and_tool_creation() -> None:
    """
    Test that validates the genie.yaml configuration and ensures the genie tool
    is properly created and accessible through the configuration.
    """
    from dao_ai.config import AppConfig

    print("\n" + "=" * 50)
    print("GENIE CONFIG VALIDATION AND TOOL CREATION TEST")
    print("=" * 50)

    try:
        # Load the genie configuration
        config_path = "config/examples/04_genie/genie_basic.yaml"
        print("\n1. Loading and validating genie configuration...")

        app_config = AppConfig.from_file(config_path)

        print("   ✓ Configuration loaded successfully")

        # Validate basic app configuration
        print("\n2. Validating app configuration...")
        assert app_config.app is not None, "App configuration should exist"
        assert app_config.app.name == "genie_basic_dao", (
            f"Expected app name 'genie_basic_dao', got '{app_config.app.name}'"
        )
        assert "genie" in app_config.app.description.lower(), (
            "App description should mention genie"
        )

        print(f"   ✓ App name: {app_config.app.name}")
        print(f"   ✓ App description: {app_config.app.description}")

        # Validate agents configuration
        print("\n3. Validating agents configuration...")
        assert len(app_config.app.agents) > 0, "Should have at least one agent"

        genie_agent = None
        for agent in app_config.app.agents:
            if hasattr(agent, "name") and agent.name == "genie":
                genie_agent = agent
                break

        assert genie_agent is not None, "Should have a genie agent"
        print(f"   ✓ Found genie agent: {genie_agent.name}")
        print(f"   ✓ Agent description: {genie_agent.description}")

        # Validate tools configuration
        print("\n4. Validating tools configuration...")
        assert len(genie_agent.tools) > 0, "Genie agent should have tools"

        # Check that genie tool is configured
        has_genie_tool = False
        for tool in genie_agent.tools:
            if hasattr(tool, "name") and "genie" in str(tool.name).lower():
                has_genie_tool = True
                print("   ✓ Found genie tool configuration")
                break

        assert has_genie_tool, "Should have genie tool configured"

        # Validate resources - genie rooms
        print("\n5. Validating genie room resources...")
        assert hasattr(app_config, "resources"), "Should have resources configuration"
        assert hasattr(app_config.resources, "genie_rooms"), (
            "Should have genie_rooms in resources"
        )
        assert len(app_config.resources.genie_rooms) > 0, (
            "Should have at least one genie room"
        )

        genie_room = list(app_config.resources.genie_rooms.values())[0]
        print(f"   ✓ Genie room name: {genie_room.name}")
        print(f"   ✓ Genie room description: {genie_room.description}")
        print(f"   ✓ Genie space ID: {genie_room.space_id}")

        # Validate the space ID matches expected format
        space_id = str(genie_room.space_id)
        expected_space_id = os.environ.get("RETAIL_AI_GENIE_SPACE_ID")
        assert len(space_id) > 0, "Space ID should not be empty"
        assert space_id == expected_space_id, (
            f"Expected space ID {expected_space_id}, got {space_id}"
        )

        print("\n6. Configuration Validation Summary:")
        print("   ✓ YAML configuration is valid and complete")
        print("   ✓ App configuration properly structured")
        print("   ✓ Genie agent properly configured with tools")
        print("   ✓ Genie room resources properly defined")
        print("   ✓ Space ID matches expected retail AI environment")

        print("\n✅ Configuration validation test PASSED!")

    except Exception as e:
        print(
            f"\n❌ Configuration validation test FAILED: {type(e).__name__}: {str(e)}"
        )
        raise


# =============================================================================
# LRUCacheService Unit Tests
# =============================================================================


class MockGenieService(GenieServiceBase):
    """Mock implementation of GenieServiceBase for testing."""

    call_count: int
    last_question: str | None
    last_conversation_id: str | None
    response_to_return: GenieResponse

    def __init__(self, response: GenieResponse | None = None) -> None:
        self.call_count = 0
        self.last_question = None
        self.last_conversation_id = None
        self.response_to_return = response or GenieResponse(
            result="Mock result",
            query="SELECT * FROM mock_table",
            description="Mock description",
            conversation_id="mock-conv-123",
        )

    def ask_question(
        self, question: str, conversation_id: str | None = None
    ) -> CacheResult:
        self.call_count += 1
        self.last_question = question
        self.last_conversation_id = conversation_id
        return CacheResult(
            response=self.response_to_return,
            cache_hit=False,
            served_by=None,
        )

    @property
    def space_id(self) -> str:
        return "test-space-id"

    @property
    def workspace_client(self) -> "WorkspaceClient | None":
        return None

    def send_feedback(
        self,
        conversation_id: str,
        rating: "GenieFeedbackRating",
        message_id: str | None = None,
        was_cache_hit: bool = False,
    ) -> None:
        """Mock implementation - does nothing."""
        pass


class MockColumn:
    """Mock column object with a name attribute."""

    def __init__(self, name: str) -> None:
        self.name = name


def create_mock_warehouse() -> Mock:
    """Create a mock WarehouseModel with mocked workspace_client."""
    mock_warehouse = Mock()
    mock_warehouse.warehouse_id = "test-warehouse-123"

    # Mock the workspace client and statement execution
    mock_ws_client = Mock()

    # Create a successful statement response
    mock_statement_response = Mock()
    mock_statement_response.status.state = StatementState.SUCCEEDED
    mock_statement_response.statement_id = "stmt-123"
    mock_statement_response.result = Mock()
    mock_statement_response.result.data_array = [
        ["value1", "value2"],
        ["value3", "value4"],
    ]
    mock_statement_response.manifest = Mock()
    mock_statement_response.manifest.schema = Mock()
    # Use real objects with name attributes for columns
    mock_statement_response.manifest.schema.columns = [
        MockColumn("col1"),
        MockColumn("col2"),
    ]

    mock_ws_client.statement_execution.execute_statement.return_value = (
        mock_statement_response
    )
    mock_warehouse.workspace_client = mock_ws_client

    return mock_warehouse


def create_mock_cache_parameters(
    warehouse: Mock | None = None,
    capacity: int = 3,
    time_to_live_seconds: int = 3600,
) -> Mock:
    """
    Create mock cache parameters that bypass Pydantic validation.

    This is necessary for unit tests because GenieLRUCacheParametersModel
    requires a valid WarehouseModel instance.
    """
    mock_params = Mock()
    mock_params.warehouse = (
        warehouse if warehouse is not None else create_mock_warehouse()
    )
    mock_params.capacity = capacity
    mock_params.time_to_live_seconds = time_to_live_seconds
    return mock_params


@pytest.fixture
def mock_warehouse_model() -> Mock:
    """Create a mock WarehouseModel with mocked workspace_client."""
    return create_mock_warehouse()


@pytest.fixture
def mock_cache_parameters(mock_warehouse_model: Mock) -> Mock:
    """Create mock cache parameters."""
    return create_mock_cache_parameters(warehouse=mock_warehouse_model)


@pytest.fixture
def mock_genie_service() -> MockGenieService:
    """Create a mock genie service."""
    return MockGenieService()


@pytest.fixture
def lru_cache_service(
    mock_genie_service: MockGenieService,
    mock_cache_parameters: Mock,
) -> LRUCacheService:
    """Create an LRUCacheService with mock dependencies."""
    return LRUCacheService(
        impl=mock_genie_service,
        parameters=mock_cache_parameters,
        name="test-cache",
    )


class TestSQLCacheEntry:
    """Tests for SQLCacheEntry dataclass."""

    @pytest.mark.unit
    def test_cache_entry_creation(self) -> None:
        """Test creating a SQLCacheEntry with all fields."""
        now = datetime.now()
        entry = SQLCacheEntry(
            query="SELECT * FROM products",
            description="Get all products",
            conversation_id="conv-123",
            created_at=now,
        )

        assert entry.query == "SELECT * FROM products"
        assert entry.description == "Get all products"
        assert entry.conversation_id == "conv-123"
        assert entry.created_at == now


class TestCacheResult:
    """Tests for CacheResult dataclass."""

    @pytest.mark.unit
    def test_cache_result_cache_hit(self) -> None:
        """Test CacheResult for a cache hit."""
        response = GenieResponse(
            result="test result",
            query="SELECT 1",
            description="test",
            conversation_id="conv-1",
        )
        result = CacheResult(
            response=response,
            cache_hit=True,
            served_by="test-cache",
        )

        assert result.response == response
        assert result.cache_hit is True
        assert result.served_by == "test-cache"

    @pytest.mark.unit
    def test_cache_result_cache_miss(self) -> None:
        """Test CacheResult for a cache miss."""
        response = GenieResponse(
            result="test result",
            query="SELECT 1",
            description="test",
            conversation_id="conv-1",
        )
        result = CacheResult(
            response=response,
            cache_hit=False,
            served_by=None,
        )

        assert result.response == response
        assert result.cache_hit is False
        assert result.served_by is None


class TestLRUCacheServiceInitialization:
    """Tests for LRUCacheService initialization."""

    @pytest.mark.unit
    def test_initialization_with_custom_name(
        self,
        mock_genie_service: MockGenieService,
        mock_cache_parameters: GenieLRUCacheParametersModel,
    ) -> None:
        """Test initialization with a custom name."""
        cache = LRUCacheService(
            impl=mock_genie_service,
            parameters=mock_cache_parameters,
            name="custom-cache-name",
        )

        assert cache.name == "custom-cache-name"
        assert cache.impl == mock_genie_service
        assert cache.parameters == mock_cache_parameters
        assert cache.size == 0

    @pytest.mark.unit
    def test_initialization_with_default_name(
        self,
        mock_genie_service: MockGenieService,
        mock_cache_parameters: GenieLRUCacheParametersModel,
    ) -> None:
        """Test initialization with default name (class name)."""
        cache = LRUCacheService(
            impl=mock_genie_service,
            parameters=mock_cache_parameters,
        )

        assert cache.name == "LRUCacheService"

    @pytest.mark.unit
    def test_properties(self, lru_cache_service: LRUCacheService) -> None:
        """Test property accessors."""
        assert lru_cache_service.capacity == 3
        assert lru_cache_service.time_to_live == timedelta(seconds=3600)
        assert lru_cache_service.warehouse is not None


class TestLRUCacheServiceKeyNormalization:
    """Tests for key normalization."""

    @pytest.mark.unit
    def test_normalize_key_strips_whitespace(self) -> None:
        """Test that normalization strips leading/trailing whitespace."""
        assert LRUCacheService._normalize_key("  hello world  ") == "hello world"

    @pytest.mark.unit
    def test_normalize_key_lowercases(self) -> None:
        """Test that normalization converts to lowercase."""
        assert LRUCacheService._normalize_key("HELLO WORLD") == "hello world"

    @pytest.mark.unit
    def test_normalize_key_combined(self) -> None:
        """Test combined normalization."""
        assert LRUCacheService._normalize_key("  HELLO World  ") == "hello world"


class TestLRUCacheServiceExpiration:
    """Tests for cache expiration logic."""

    @pytest.mark.unit
    def test_is_expired_fresh_entry(self, lru_cache_service: LRUCacheService) -> None:
        """Test that a fresh entry is not expired."""
        entry = SQLCacheEntry(
            query="SELECT 1",
            description="test",
            conversation_id="conv-1",
            created_at=datetime.now(),
        )
        assert lru_cache_service._is_expired(entry) is False

    @pytest.mark.unit
    def test_is_expired_old_entry(self, lru_cache_service: LRUCacheService) -> None:
        """Test that an old entry is expired."""
        entry = SQLCacheEntry(
            query="SELECT 1",
            description="test",
            conversation_id="conv-1",
            created_at=datetime.now() - timedelta(hours=2),  # TTL is 1 hour
        )
        assert lru_cache_service._is_expired(entry) is True


class TestLRUCacheServiceCacheOperations:
    """Tests for basic cache operations."""

    @pytest.mark.unit
    def test_cache_miss_calls_impl(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that a cache miss delegates to the wrapped service."""
        result = lru_cache_service.ask_question("What is the total sales?")

        assert mock_genie_service.call_count == 1
        assert mock_genie_service.last_question == "What is the total sales?"
        assert result.response.query == "SELECT * FROM mock_table"
        assert lru_cache_service.size == 1

    @pytest.mark.unit
    def test_cache_hit_does_not_call_impl(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that a cache hit does not call the wrapped service."""
        # First call - cache miss
        lru_cache_service.ask_question("What is the total sales?")
        assert mock_genie_service.call_count == 1

        # Second call - cache hit
        lru_cache_service.ask_question("What is the total sales?")
        assert mock_genie_service.call_count == 1  # Still 1, not called again

    @pytest.mark.unit
    def test_cache_hit_with_different_case(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that cache keys are case-insensitive."""
        # First call - cache miss
        lru_cache_service.ask_question("What is the total sales?")
        assert mock_genie_service.call_count == 1

        # Second call with different case - should still be cache hit
        lru_cache_service.ask_question("WHAT IS THE TOTAL SALES?")
        assert mock_genie_service.call_count == 1

    @pytest.mark.unit
    def test_cache_hit_with_whitespace_variation(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that cache keys ignore leading/trailing whitespace."""
        # First call - cache miss
        lru_cache_service.ask_question("What is the total sales?")
        assert mock_genie_service.call_count == 1

        # Second call with whitespace - should still be cache hit
        lru_cache_service.ask_question("  What is the total sales?  ")
        assert mock_genie_service.call_count == 1

    @pytest.mark.unit
    def test_lru_eviction_at_capacity(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that oldest entries are evicted when at capacity."""
        # Fill cache to capacity (3)
        lru_cache_service.ask_question("question 1")
        lru_cache_service.ask_question("question 2")
        lru_cache_service.ask_question("question 3")
        assert lru_cache_service.size == 3
        assert mock_genie_service.call_count == 3

        # Add one more - should evict "question 1"
        lru_cache_service.ask_question("question 4")
        assert lru_cache_service.size == 3  # Still at capacity
        assert mock_genie_service.call_count == 4

        # "question 1" should now be a cache miss
        lru_cache_service.ask_question("question 1")
        assert mock_genie_service.call_count == 5  # Had to call impl again

        # "question 2" should now be evicted
        lru_cache_service.ask_question("question 2")
        assert mock_genie_service.call_count == 6

    @pytest.mark.unit
    def test_lru_access_updates_recency(
        self,
        lru_cache_service: LRUCacheService,
        mock_genie_service: MockGenieService,
    ) -> None:
        """Test that accessing an entry updates its recency."""
        # Fill cache
        lru_cache_service.ask_question("question 1")
        lru_cache_service.ask_question("question 2")
        lru_cache_service.ask_question("question 3")

        # Access question 1 again - moves to end
        lru_cache_service.ask_question("question 1")

        # Add new question - should evict question 2 (oldest now)
        lru_cache_service.ask_question("question 4")

        # question 1 should still be in cache
        lru_cache_service.ask_question("question 1")
        assert mock_genie_service.call_count == 4  # No new call

        # question 2 should be evicted
        lru_cache_service.ask_question("question 2")
        assert mock_genie_service.call_count == 5  # Had to call impl

    @pytest.mark.unit
    def test_expired_entry_causes_cache_miss(
        self,
        mock_genie_service: MockGenieService,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test that expired entries result in cache miss."""
        # Create cache with very short TTL
        params = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=10,
            time_to_live_seconds=1,  # 1 second TTL
        )
        cache = LRUCacheService(
            impl=mock_genie_service,
            parameters=params,
        )

        # First call - cache miss
        cache.ask_question("test question")
        assert mock_genie_service.call_count == 1

        # Manually expire the entry by modifying created_at
        key = cache._normalize_key("test question")
        with cache._lock:
            cache._cache[key].created_at = datetime.now() - timedelta(seconds=5)

        # Second call - should be cache miss due to expiration
        cache.ask_question("test question")
        assert mock_genie_service.call_count == 2


class TestLRUCacheServiceWithCacheInfo:
    """Tests for ask_question_with_cache_info method."""

    @pytest.mark.unit
    def test_cache_miss_returns_correct_info(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test that cache miss returns correct CacheResult."""
        result = lru_cache_service.ask_question_with_cache_info("new question")

        assert result.cache_hit is False
        assert result.served_by is None
        assert result.response.query == "SELECT * FROM mock_table"

    @pytest.mark.unit
    def test_cache_hit_returns_correct_info(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test that cache hit returns correct CacheResult."""
        # First call - cache miss
        lru_cache_service.ask_question("test question")

        # Second call - cache hit
        result = lru_cache_service.ask_question_with_cache_info("test question")

        assert result.cache_hit is True
        assert result.served_by == "test-cache"


class TestLRUCacheServiceManagement:
    """Tests for cache management operations."""

    @pytest.mark.unit
    def test_invalidate_existing_entry(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test invalidating an existing cache entry."""
        lru_cache_service.ask_question("test question")
        assert lru_cache_service.size == 1

        result = lru_cache_service.invalidate("test question")
        assert result is True
        assert lru_cache_service.size == 0

    @pytest.mark.unit
    def test_invalidate_nonexistent_entry(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test invalidating a non-existent cache entry."""
        result = lru_cache_service.invalidate("nonexistent")
        assert result is False

    @pytest.mark.unit
    def test_clear_removes_all_entries(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test clearing all cache entries."""
        lru_cache_service.ask_question("question 1")
        lru_cache_service.ask_question("question 2")
        assert lru_cache_service.size == 2

        count = lru_cache_service.clear()
        assert count == 2
        assert lru_cache_service.size == 0

    @pytest.mark.unit
    def test_stats_returns_correct_info(
        self,
        lru_cache_service: LRUCacheService,
    ) -> None:
        """Test stats method returns correct information."""
        lru_cache_service.ask_question("question 1")
        lru_cache_service.ask_question("question 2")

        stats = lru_cache_service.stats()

        assert stats["size"] == 2
        assert stats["capacity"] == 3
        assert stats["ttl_seconds"] == 3600.0
        assert stats["expired_entries"] == 0
        assert stats["valid_entries"] == 2


class TestLRUCacheServiceSQLExecution:
    """Tests for SQL execution on cache hit."""

    @pytest.mark.unit
    def test_cache_hit_executes_sql(
        self,
        lru_cache_service: LRUCacheService,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test that cache hit executes cached SQL via warehouse."""
        # First call - cache miss
        lru_cache_service.ask_question("test question")

        # Second call - cache hit, should execute SQL
        result = lru_cache_service.ask_question("test question")

        # Verify SQL was executed
        mock_warehouse_model.workspace_client.statement_execution.execute_statement.assert_called()

        # Verify response contains data from SQL execution
        assert isinstance(result.response.result, pd.DataFrame)
        assert list(result.response.result.columns) == ["col1", "col2"]

    @pytest.mark.unit
    def test_sql_execution_failure_falls_back_to_genie(
        self,
        mock_genie_service: MockGenieService,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test that SQL execution failure triggers fallback to Genie.

        When a cached SQL query fails to execute (e.g., table was dropped),
        the cache should:
        1. Invalidate the stale cache entry
        2. Fall back to Genie for fresh SQL
        3. Return a valid response (not an error)
        """
        # Configure failed SQL execution
        mock_statement_response = Mock()
        mock_statement_response.status.state = StatementState.FAILED
        mock_statement_response.status.error = Mock(message="SQL syntax error")
        mock_warehouse_model.workspace_client.statement_execution.execute_statement.return_value = mock_statement_response

        params = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=10,
            time_to_live_seconds=3600,
        )
        cache = LRUCacheService(
            impl=mock_genie_service,
            parameters=params,
        )

        # First call - cache miss, stores in cache
        cache.ask_question("test question")
        assert cache.size == 1
        assert mock_genie_service.call_count == 1

        # Second call - cache hit, SQL execution fails, falls back to Genie
        result = cache.ask_question("test question")

        # Verify fallback behavior:
        # 1. Genie was called again (fallback)
        assert mock_genie_service.call_count == 2
        # 2. Response is valid (from Genie, not an error)
        assert result.response.result == "Mock result"
        # 3. Cache entry was re-added with fresh SQL
        assert cache.size == 1


# =============================================================================
# LRUCacheService Integration Tests
# =============================================================================


class TestLRUCacheServiceIntegration:
    """Integration tests for LRUCacheService."""

    @pytest.mark.integration
    def test_full_cache_flow(
        self,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test full cache flow: miss -> hit -> eviction -> miss."""
        mock_service = MockGenieService()
        params = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=2,
            time_to_live_seconds=3600,
        )
        cache = LRUCacheService(impl=mock_service, parameters=params)

        # Step 1: Cache miss for question 1
        result1 = cache.ask_question_with_cache_info("question 1")
        assert result1.cache_hit is False
        assert mock_service.call_count == 1

        # Step 2: Cache hit for question 1
        result2 = cache.ask_question_with_cache_info("question 1")
        assert result2.cache_hit is True
        assert mock_service.call_count == 1  # No new call

        # Step 3: Cache miss for question 2
        result3 = cache.ask_question_with_cache_info("question 2")
        assert result3.cache_hit is False
        assert mock_service.call_count == 2

        # Step 4: Cache miss for question 3 (evicts question 1)
        result4 = cache.ask_question_with_cache_info("question 3")
        assert result4.cache_hit is False
        assert mock_service.call_count == 3
        assert cache.size == 2

        # Step 5: Cache miss for question 1 (was evicted)
        result5 = cache.ask_question_with_cache_info("question 1")
        assert result5.cache_hit is False
        assert mock_service.call_count == 4

    @pytest.mark.integration
    def test_chained_cache_services(
        self,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test chaining multiple cache services."""
        # Create base service
        mock_service = MockGenieService()

        # Create first cache layer
        params1 = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=10,
            time_to_live_seconds=3600,
        )
        cache1 = LRUCacheService(
            impl=mock_service,
            parameters=params1,
            name="cache-layer-1",
        )

        # Create second cache layer wrapping the first
        params2 = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=5,
            time_to_live_seconds=1800,
        )
        cache2 = LRUCacheService(
            impl=cache1,
            parameters=params2,
            name="cache-layer-2",
        )

        # First call - misses both caches
        result1 = cache2.ask_question_with_cache_info("test question")
        assert result1.cache_hit is False
        assert mock_service.call_count == 1
        assert cache1.size == 1
        assert cache2.size == 1

        # Second call - hits outer cache (cache2)
        result2 = cache2.ask_question_with_cache_info("test question")
        assert result2.cache_hit is True
        assert result2.served_by == "cache-layer-2"
        assert mock_service.call_count == 1  # No new call

    @pytest.mark.integration
    def test_conversation_id_preserved(
        self,
        mock_warehouse_model: Mock,
    ) -> None:
        """Test that conversation_id is preserved through cache."""
        custom_response = GenieResponse(
            result="Custom result",
            query="SELECT * FROM custom",
            description="Custom description",
            conversation_id="custom-conv-456",
        )
        mock_service = MockGenieService(response=custom_response)

        params = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=10,
            time_to_live_seconds=3600,
        )
        cache = LRUCacheService(impl=mock_service, parameters=params)

        # First call - cache miss
        result1 = cache.ask_question("test")
        assert result1.response.conversation_id == "custom-conv-456"

        # Second call - cache hit
        result2 = cache.ask_question("test")
        assert result2.response.conversation_id == "custom-conv-456"

    @pytest.mark.integration
    def test_thread_safety_basic(
        self,
        mock_warehouse_model: Mock,
    ) -> None:
        """Basic test for thread safety of cache operations."""
        import threading

        mock_service = MockGenieService()
        params = create_mock_cache_parameters(
            warehouse=mock_warehouse_model,
            capacity=100,
            time_to_live_seconds=3600,
        )
        cache = LRUCacheService(impl=mock_service, parameters=params)

        results: list[GenieResponse] = []
        errors: list[Exception] = []

        def worker(question: str) -> None:
            try:
                response = cache.ask_question(question)
                results.append(response)
            except Exception as e:
                errors.append(e)

        # Create multiple threads making concurrent requests
        threads: list[threading.Thread] = []
        for i in range(10):
            t = threading.Thread(target=worker, args=(f"question {i % 3}",))
            threads.append(t)

        # Start all threads
        for t in threads:
            t.start()

        # Wait for all threads to complete
        for t in threads:
            t.join()

        # Verify no errors occurred
        assert len(errors) == 0, f"Errors during concurrent access: {errors}"
        assert len(results) == 10
        # Only 3 unique questions, so at most 3 calls to impl
        assert mock_service.call_count <= 10


@pytest.mark.integration
@pytest.mark.skipif(not has_retail_ai_env(), reason="Retail AI env vars not set")
def test_create_genie_tool_with_cache_parameters() -> None:
    """Test creating a genie tool with LRU cache parameters."""
    from dao_ai.config import WarehouseModel

    # Create a mock warehouse model for testing
    # In real integration, you would use actual Databricks credentials
    with patch("dao_ai.tools.genie.Genie") as mock_genie_class:
        mock_genie_instance = Mock()
        mock_genie_instance.ask_question = Mock(
            return_value=GenieResponse(
                result="Test result",
                query="SELECT 1",
                description="Test",
                conversation_id="test-conv",
            )
        )
        mock_genie_class.return_value = mock_genie_instance

        genie_room = GenieRoomModel(
            name="Test Room",
            space_id=os.environ.get("RETAIL_AI_GENIE_SPACE_ID"),
        )

        # Create mock warehouse with IsDatabricksResource attrs
        mock_warehouse = Mock(spec=WarehouseModel)
        mock_warehouse.name = "Test Warehouse"
        mock_warehouse.warehouse_id = "test-warehouse"
        mock_warehouse.workspace_client = Mock()
        add_databricks_resource_attrs(mock_warehouse)

        cache_params = GenieLRUCacheParametersModel(
            warehouse=mock_warehouse,
            capacity=50,
            time_to_live_seconds=7200,
        )

        tool = create_genie_tool(
            genie_room=genie_room,
            name="cached_genie_tool",
            lru_cache_parameters=cache_params,
        )

        assert isinstance(tool, StructuredTool)
        assert tool.name == "cached_genie_tool"


# =============================================================================
# PostgresContextAwareGenieService Tests
# =============================================================================


class MockPostgresPool:
    """Mock PostgreSQL connection pool for testing.

    Mimics psycopg pool with row_factory=dict_row, returning dicts.
    """

    def __init__(self) -> None:
        self.executed_queries: list[tuple[str, tuple]] = []
        self.query_results: list[dict | None] = []
        self._result_index = 0
        self._current_cursor: "MockCursor | None" = None

    def set_query_results(self, results: list[dict | None]) -> None:
        """Set the results to return for subsequent SELECT/COUNT queries."""
        self.query_results = results
        self._result_index = 0

    def get_next_result(self) -> dict | None:
        """Get the next result from the queue."""
        if self.query_results and self._result_index < len(self.query_results):
            result = self.query_results[self._result_index]
            self._result_index += 1
            return result
        return None

    def connection(self) -> "MockConnection":
        return MockConnection(self)


class MockConnection:
    """Mock database connection."""

    def __init__(self, pool: MockPostgresPool) -> None:
        self.pool = pool

    def __enter__(self) -> "MockConnection":
        return self

    def __exit__(self, *args: object) -> None:
        pass

    def cursor(self) -> "MockCursor":
        return MockCursor(self.pool)


class MockCursor:
    """Mock database cursor (mimics psycopg cursor with dict_row)."""

    def __init__(self, pool: MockPostgresPool) -> None:
        self.pool = pool
        self._last_result: dict | None = None
        self.rowcount: int = 0

    def __enter__(self) -> "MockCursor":
        return self

    def __exit__(self, *args: object) -> None:
        pass

    def execute(self, query: str, params: tuple = ()) -> None:
        # Convert psycopg.sql.Composed objects to plain strings
        if not isinstance(query, str):
            query = query.as_string(None).replace('"', "")
        self.pool.executed_queries.append((query, params))
        query_stripped = query.strip().upper()
        # SET LOCAL statements don't return results and shouldn't consume queue
        if query_stripped.startswith("SET"):
            self._last_result = None
            self.rowcount = 0
        # SELECT or WITH (CTEs) return results from the queue
        elif query_stripped.startswith("SELECT") or query_stripped.startswith("WITH"):
            self._last_result = self.pool.get_next_result()
            self.rowcount = 1 if self._last_result else 0
        else:
            self._last_result = None
            self.rowcount = 1  # Assume successful for non-SELECT queries

    def fetchone(self) -> dict | None:
        return self._last_result

    def fetchall(self) -> list[dict]:
        if self._last_result is None:
            return []
        # Support returning a single dict as a one-element list
        if isinstance(self._last_result, list):
            return self._last_result
        return [self._last_result]


class MockEmbeddings:
    """Mock embeddings model for testing."""

    def __init__(self, dims: int = 1024) -> None:
        self.dims = dims
        self.embed_calls: list[list[str]] = []

    def embed_documents(self, texts: list[str]) -> list[list[float]]:
        self.embed_calls.append(texts)
        # Return deterministic embeddings based on text content
        return [[hash(text) % 1000 / 1000.0] * self.dims for text in texts]


def create_mock_context_aware_cache_parameters(
    database: Mock,
    warehouse: Mock,
    time_to_live_seconds: int = 86400,
    similarity_threshold: float = 0.85,
    embedding_dims: int = 1024,
    table_name: str = "test_context_aware_cache",
) -> GenieContextAwareCacheParametersModel:
    """Create a mock GenieContextAwareCacheParametersModel for testing."""
    return GenieContextAwareCacheParametersModel(
        database=database,
        warehouse=warehouse,
        time_to_live_seconds=time_to_live_seconds,
        similarity_threshold=similarity_threshold,
        embedding_dims=embedding_dims,
        table_name=table_name,
    )


class TestPostgresContextAwareCacheInitialization:
    """Tests for PostgresContextAwareGenieService initialization."""

    def test_init_with_defaults(self) -> None:
        """Test initialization with default name."""
        mock_service = MockGenieService()
        mock_database = Mock()
        mock_database.name = "test_db"
        mock_database.connection_params = {"host": "localhost"}
        mock_database.connection_kwargs = {}
        mock_database.max_pool_size = 10
        mock_database.timeout_seconds = 10

        mock_warehouse = Mock()
        mock_warehouse.warehouse_id = "test-warehouse"
        mock_warehouse.workspace_client = Mock()

        # Mock the parameters model - we'll create a real one with mocked dependencies
        with patch(
            "dao_ai.config.GenieContextAwareCacheParametersModel.__init__",
            return_value=None,
        ):
            params = Mock(spec=GenieContextAwareCacheParametersModel)
            params.database = mock_database
            params.warehouse = mock_warehouse
            params.time_to_live_seconds = 86400
            params.similarity_threshold = 0.85
            params.context_similarity_threshold = 0.80
            params.question_weight = 0.6
            params.context_weight = 0.4
            params.embedding_dims = 1024
            params.embedding_model = "databricks-gte-large-en"
            params.table_name = "test_cache"
            params.prompt_history_table = "test_prompt_history"
            params.max_prompt_history_length = 50
            params.use_genie_api_for_history = False
            params.prompt_history_ttl_seconds = None
            params.ivfflat_lists = 100
            params.ivfflat_probes = 10
            params.ivfflat_candidates = 20

            cache = PostgresContextAwareGenieService(
                impl=mock_service, parameters=params
            )

            assert cache.impl is mock_service
            assert cache.name == "PostgresContextAwareGenieService"
            assert cache.space_id == "test-space-id"  # From MockGenieService
            assert cache._setup_complete is False

    def test_init_with_custom_name(self) -> None:
        """Test initialization with custom name."""
        mock_service = MockGenieService()
        params = Mock(spec=GenieContextAwareCacheParametersModel)

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
            name="CustomContextAwareCache",
        )

        assert cache.name == "CustomContextAwareCache"


class TestPostgresContextAwareCacheProperties:
    """Tests for PostgresContextAwareGenieService properties."""

    def test_database_property(self) -> None:
        """Test database property returns correct value."""
        mock_service = MockGenieService()
        mock_database = Mock()

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = mock_database

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.database is mock_database

    def test_warehouse_property(self) -> None:
        """Test warehouse property returns correct value."""
        mock_service = MockGenieService()
        mock_warehouse = Mock()

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.warehouse = mock_warehouse

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.warehouse is mock_warehouse

    def test_time_to_live_property(self) -> None:
        """Test time_to_live returns timedelta."""
        mock_service = MockGenieService()

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.time_to_live_seconds = 7200

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.time_to_live == timedelta(seconds=7200)

    def test_similarity_threshold_property(self) -> None:
        """Test similarity_threshold property returns correct value."""
        mock_service = MockGenieService()

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.similarity_threshold = 0.9
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.similarity_threshold == 0.9

    def test_table_name_property(self) -> None:
        """Test table_name property returns correct value."""
        mock_service = MockGenieService()

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.table_name = "custom_cache_table"

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.table_name == "custom_cache_table"


class TestPostgresContextAwareCacheOperations:
    """Tests for PostgresContextAwareGenieService cache lookup and storage operations."""

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_cache_miss_stores_entry(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that cache miss stores the new entry."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        # Setup mocks
        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        # First query returns None (dimension check - table doesn't exist)
        # Second query returns 0 rows (for table row count check)
        # Third query returns None (no similar entry found)
        mock_pool.set_query_results([None, None])

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.context_window_size = 3
        params.max_context_tokens = 2000
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        ).initialize()
        result = cache.ask_question_with_cache_info("What is the inventory?")

        # Verify cache miss
        assert result.cache_hit is False
        assert result.served_by is None

        # Verify entry was stored (INSERT query was executed)
        insert_queries = [
            q for q, _ in mock_pool.executed_queries if "INSERT INTO" in q
        ]
        assert len(insert_queries) >= 1

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_cache_hit_returns_cached_entry(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that cache hit returns cached SQL and re-executes it."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        # Setup mocks
        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        # Create warehouse mock with statement execution
        from datetime import timezone

        cached_time = datetime.now(timezone.utc)

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()

        # Mock statement execution response
        mock_statement_response = Mock()
        mock_statement_response.status.state = StatementState.SUCCEEDED
        mock_statement_response.result.data_array = [["value1", "value2"]]
        mock_statement_response.manifest.schema.columns = [
            Mock(name="col1"),
            Mock(name="col2"),
        ]
        mock_workspace_client.statement_execution.execute_statement.return_value = (
            mock_statement_response
        )

        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"

        params.warehouse = mock_warehouse
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.context_window_size = 3
        params.max_context_tokens = 2000
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2. row count for auto-computing ivfflat lists
        # 3-6. _index_exists for 4 main table indexes (None each)
        # 7-10. _index_exists for 4 prompt history table indexes (None each)
        # 11. cache lookup (ORDER BY ... LIMIT K) - returns cached entry
        # SET LOCAL is handled by MockCursor without consuming a result
        mock_pool.set_query_results(
            [
                None,  # dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {
                    "id": 1,
                    "question": "Similar question?",
                    "conversation_context": "",
                    "sql_query": "SELECT * FROM inventory",
                    "description": "Cached description",
                    "conversation_id": "cached-conv-123",
                    "created_at": cached_time,
                    "message_id": None,
                    "question_distance": 0.08,  # 1 - 0.92 = 0.08
                    "context_distance": 0.08,
                },
            ]
        )

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        ).initialize()
        result = cache.ask_question_with_cache_info("What is the inventory?")

        # Verify cache hit
        assert result.cache_hit is True
        assert result.served_by == "PostgresContextAwareGenieService"

        # Verify SQL was executed via warehouse
        mock_workspace_client.statement_execution.execute_statement.assert_called_once()

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_cache_hit_below_threshold_is_miss(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that similarity below threshold results in cache miss."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        # Setup mocks
        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        from datetime import timezone

        cached_time = datetime.now(timezone.utc)

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.warehouse.warehouse_id = "test-warehouse"
        params.warehouse.workspace_client = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2-5. _index_exists for 4 main table indexes (None each)
        # 6-9. _index_exists for 4 prompt history table indexes (None each)
        # 10. cache lookup - returns entry with question below threshold
        mock_pool.set_query_results(
            [
                None,  # dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {
                    "id": 1,
                    "question": "Different question",
                    "conversation_context": "",
                    "sql_query": "SELECT * FROM inventory",
                    "description": "Description",
                    "conversation_id": "conv-123",
                    "created_at": cached_time,
                    "message_id": None,
                    "question_distance": 0.25,  # 1 - 0.75 = 0.25 (below 0.85 threshold)
                    "context_distance": 0.15,
                },
            ]
        )

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        ).initialize()
        result = cache.ask_question_with_cache_info("What is the inventory?")

        # Should be a miss because similarity is below threshold
        assert result.cache_hit is False

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_expired_entry_deleted_and_refreshed(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that expired entries are deleted and trigger a refresh (miss)."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        # Setup mocks
        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        from datetime import timezone

        old_time = datetime.now(timezone.utc) - timedelta(days=2)  # Older than TTL

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400  # 1 day
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2-5. _index_exists for 4 main table indexes (None each)
        # 6-9. _index_exists for 4 prompt history table indexes (None each)
        # 10. cache lookup - returns expired entry (old_time > TTL)
        mock_pool.set_query_results(
            [
                None,  # dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {
                    "id": 123,  # ID used for deletion
                    "question": "Similar question?",
                    "conversation_context": "",
                    "sql_query": "SELECT * FROM inventory",
                    "description": "Description",
                    "conversation_id": "conv-123",
                    "created_at": old_time,
                    "message_id": None,
                    "question_distance": 0.05,  # 1 - 0.95 = high similarity
                    "context_distance": 0.05,
                },
            ]
        )

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        ).initialize()
        result = cache.ask_question_with_cache_info("What is the inventory?")

        # Should be a miss because entry is expired (triggers refresh)
        assert result.cache_hit is False

        # Verify DELETE query was executed
        delete_queries = [
            q for q, _ in mock_pool.executed_queries if "DELETE" in q.upper()
        ]
        assert len(delete_queries) >= 1


class TestPostgresContextAwareCacheManagement:
    """Tests for cache management operations."""

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_clear_deletes_all_entries(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that clear() removes all entries."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.context_window_size = 3
        params.max_context_tokens = 2000
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # First query for dimension check, second for table creation
        mock_pool.set_query_results([None])

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        cache.clear()

        # Verify DELETE query was executed
        delete_queries = [q for q, _ in mock_pool.executed_queries if "DELETE" in q]
        assert len(delete_queries) >= 1

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_invalidate_expired_removes_old_entries(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that invalidate_expired() removes expired entries."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 3600  # 1 hour TTL
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # First query for dimension check, second for table creation
        mock_pool.set_query_results([None])

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        cache.invalidate_expired()

        # Verify DELETE with INTERVAL was executed
        delete_queries = [
            (q, p)
            for q, p in mock_pool.executed_queries
            if "DELETE" in q and "INTERVAL" in q
        ]
        assert len(delete_queries) >= 1
        # Verify space_id (from impl) and TTL were passed as parameters
        assert delete_queries[0][1] == (
            "test-space-id",
            3600,
        )  # From MockGenieService.space_id

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_size_returns_count(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that size property returns correct count."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.context_window_size = 3
        params.max_context_tokens = 2000
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2-5. _index_exists for 4 main table indexes (None each)
        # 6-9. _index_exists for 4 prompt history table indexes (None each)
        # 10. count query for size
        mock_pool.set_query_results(
            [
                None,  # dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {"count": 42},
            ]
        )

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        assert cache.size == 42

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_stats_returns_statistics(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that stats() returns cache statistics."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "test_cache"
        params.context_window_size = 3
        params.max_context_tokens = 2000
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2-5. _index_exists for 4 main table indexes (None each)
        # 6-9. _index_exists for 4 prompt history table indexes (None each)
        # 10. stats query
        mock_pool.set_query_results(
            [
                None,  # dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {"total": 100, "valid": 95, "expired": 5},
            ]
        )

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        )
        stats = cache.stats()

        assert stats["size"] == 100
        assert stats["valid_entries"] == 95
        assert stats["expired_entries"] == 5
        assert stats["ttl_seconds"] == 86400.0
        assert stats["similarity_threshold"] == 0.85


class TestPostgresContextAwareCacheTableCreation:
    """Tests for table creation behavior."""

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_creates_table_on_first_use(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that table is created on first cache access."""
        mock_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        params = Mock(spec=GenieContextAwareCacheParametersModel)
        params.database = Mock()
        params.warehouse = Mock()
        params.time_to_live_seconds = 86400
        params.similarity_threshold = 0.85
        params.context_similarity_threshold = 0.80
        params.question_weight = 0.6
        params.context_weight = 0.4
        params.embedding_dims = 1024
        params.embedding_model = "databricks-gte-large-en"
        params.table_name = "my_context_aware_cache"
        params.prompt_history_table = "test_prompt_history"
        params.max_prompt_history_length = 50
        params.use_genie_api_for_history = False
        params.prompt_history_ttl_seconds = None
        params.ivfflat_lists = 100
        params.ivfflat_probes = 10
        params.ivfflat_candidates = 20

        # Dimension check returns None (table doesn't exist), table count returns 0
        mock_pool.set_query_results([None, None])

        cache = PostgresContextAwareGenieService(
            impl=mock_service,
            parameters=params,
        ).initialize()
        cache.ask_question("Test question")

        # Verify CREATE EXTENSION and CREATE TABLE were executed
        create_queries = [q for q, _ in mock_pool.executed_queries if "CREATE" in q]
        assert any("CREATE EXTENSION" in q and "vector" in q for q in create_queries)
        assert any(
            "CREATE TABLE" in q and "my_context_aware_cache" in q
            for q in create_queries
        )


class TestLRUPlusContextAwareCacheIntegration:
    """Integration tests for LRU + context-aware cache combination (two-tier caching)."""

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_both_caches_store_on_miss(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that both LRU and context-aware caches store entry on complete miss."""
        # Create base Genie service
        mock_genie_service = MockGenieService()
        mock_pool = MockPostgresPool()

        # Setup mocks
        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        # Setup context-aware cache parameters
        semantic_params = Mock(spec=GenieContextAwareCacheParametersModel)
        semantic_params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()
        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"
        semantic_params.warehouse = mock_warehouse
        semantic_params.time_to_live_seconds = 86400
        semantic_params.similarity_threshold = 0.85
        semantic_params.context_similarity_threshold = 0.80
        semantic_params.question_weight = 0.6
        semantic_params.context_weight = 0.4
        semantic_params.embedding_dims = 1024
        semantic_params.embedding_model = "databricks-gte-large-en"
        semantic_params.table_name = "test_cache"
        semantic_params.context_window_size = 3
        semantic_params.max_context_tokens = 2000
        semantic_params.prompt_history_table = "test_prompt_history"
        semantic_params.max_prompt_history_length = 50
        semantic_params.use_genie_api_for_history = False
        semantic_params.prompt_history_ttl_seconds = None
        semantic_params.ivfflat_lists = 100
        semantic_params.ivfflat_probes = 10
        semantic_params.ivfflat_candidates = 20

        # Dimension check, table count returns 0, then no similar entry found
        mock_pool.set_query_results([None, None])

        # Create context-aware cache wrapping Genie
        context_aware_cache = PostgresContextAwareGenieService(
            impl=mock_genie_service,
            parameters=semantic_params,
            name="ContextAwareCache",
        ).initialize()

        # Setup LRU cache parameters
        lru_params = Mock(spec=GenieLRUCacheParametersModel)
        lru_params.warehouse = mock_warehouse
        lru_params.capacity = 100
        lru_params.time_to_live_seconds = 3600

        # Create LRU cache wrapping context-aware cache (checked first)
        lru_cache = LRUCacheService(
            impl=context_aware_cache,
            parameters=lru_params,
            name="LRUCache",
        )

        # First call - misses both caches
        result = lru_cache.ask_question_with_cache_info("What is the inventory count?")

        # Verify cache miss
        assert result.cache_hit is False
        assert mock_genie_service.call_count == 1

        # Verify LRU cache stored entry
        assert lru_cache.size == 1

        # Verify context-aware cache stored entry (check INSERT query)
        insert_queries = [
            q for q, _ in mock_pool.executed_queries if "INSERT INTO" in q
        ]
        assert len(insert_queries) >= 1, "Context-aware cache should have stored entry"

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_lru_hit_skips_semantic(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that LRU hit returns immediately without checking context-aware cache."""
        mock_genie_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        # Setup context-aware cache
        semantic_params = Mock(spec=GenieContextAwareCacheParametersModel)
        semantic_params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()

        # Mock SQL execution for LRU cache hit
        mock_statement_response = Mock()
        mock_statement_response.status.state = StatementState.SUCCEEDED
        mock_statement_response.result.data_array = [["value1"]]
        mock_statement_response.manifest.schema.columns = [Mock(name="col1")]
        mock_workspace_client.statement_execution.execute_statement.return_value = (
            mock_statement_response
        )

        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"
        semantic_params.warehouse = mock_warehouse
        semantic_params.time_to_live_seconds = 86400
        semantic_params.similarity_threshold = 0.85
        semantic_params.context_similarity_threshold = 0.80
        semantic_params.question_weight = 0.6
        semantic_params.context_weight = 0.4
        semantic_params.embedding_dims = 1024
        semantic_params.embedding_model = "databricks-gte-large-en"
        semantic_params.table_name = "test_cache"
        semantic_params.context_window_size = 3
        semantic_params.max_context_tokens = 2000
        semantic_params.prompt_history_table = "test_prompt_history"
        semantic_params.max_prompt_history_length = 50
        semantic_params.use_genie_api_for_history = False
        semantic_params.prompt_history_ttl_seconds = None
        semantic_params.ivfflat_lists = 100
        semantic_params.ivfflat_probes = 10
        semantic_params.ivfflat_candidates = 20

        mock_pool.set_query_results([None, None])

        context_aware_cache = PostgresContextAwareGenieService(
            impl=mock_genie_service,
            parameters=semantic_params,
            workspace_client=None,  # No context for this test
        ).initialize()

        lru_params = Mock(spec=GenieLRUCacheParametersModel)
        lru_params.warehouse = mock_warehouse
        lru_params.capacity = 100
        lru_params.time_to_live_seconds = 3600

        lru_cache = LRUCacheService(
            impl=context_aware_cache,
            parameters=lru_params,
        )

        # First call - miss both caches
        question = "What is the inventory count?"
        lru_cache.ask_question(question)
        assert mock_genie_service.call_count == 1

        # Record queries executed so far
        queries_after_first_call = len(mock_pool.executed_queries)

        # Second call with exact same question - should hit LRU
        result = lru_cache.ask_question_with_cache_info(question)

        assert result.cache_hit is True
        assert result.served_by == "LRUCacheService"
        assert mock_genie_service.call_count == 1  # No new Genie calls

        # Context-aware cache should NOT have been queried (no new SELECT queries)
        select_queries_after_second = [
            q
            for q, _ in mock_pool.executed_queries[queries_after_first_call:]
            if "SELECT" in q and "question_embedding" in q
        ]
        assert len(select_queries_after_second) == 0, (
            "Context-aware cache should not be queried on LRU hit"
        )

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_lru_miss_semantic_hit(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test LRU miss but context-aware cache hit for similar question."""
        from datetime import timezone

        mock_genie_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        semantic_params = Mock(spec=GenieContextAwareCacheParametersModel)
        semantic_params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()

        # Mock SQL execution
        mock_statement_response = Mock()
        mock_statement_response.status.state = StatementState.SUCCEEDED
        mock_statement_response.result.data_array = [["fresh_value"]]
        mock_statement_response.manifest.schema.columns = [Mock(name="result")]
        mock_workspace_client.statement_execution.execute_statement.return_value = (
            mock_statement_response
        )

        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"
        semantic_params.warehouse = mock_warehouse
        semantic_params.time_to_live_seconds = 86400
        semantic_params.similarity_threshold = 0.85
        semantic_params.context_similarity_threshold = 0.80
        semantic_params.question_weight = 0.6
        semantic_params.context_weight = 0.4
        semantic_params.embedding_dims = 1024
        semantic_params.embedding_model = "databricks-gte-large-en"
        semantic_params.table_name = "test_cache"
        semantic_params.context_window_size = 3
        semantic_params.max_context_tokens = 2000
        semantic_params.prompt_history_table = "test_prompt_history"
        semantic_params.max_prompt_history_length = 50
        semantic_params.use_genie_api_for_history = False
        semantic_params.prompt_history_ttl_seconds = None
        semantic_params.ivfflat_lists = 100
        semantic_params.ivfflat_probes = 10
        semantic_params.ivfflat_candidates = 20

        cached_time = datetime.now(timezone.utc)

        # Query sequence for this test (MockCursor only consumes results for SELECT queries):
        # Initialization:
        #   1. Dimension check SELECT (consumes result)
        #   2-5. _index_exists for 4 main table indexes (None each)
        #   6-9. _index_exists for 4 prompt history table indexes (None each)
        # First call (ask_question "What is the inventory?"):
        #   - SET LOCAL (no result consumed)
        #   10. find_similar SELECT (consumes result) - miss
        #   - INSERT for cache entry (no result consumed)
        #   - INSERT for prompt history (no result consumed)
        # Second call (ask_question "Show me inventory count"):
        #   - SET LOCAL (no result consumed)
        #   11. find_similar SELECT (consumes result) - hit!
        #   - INSERT for prompt history (no result consumed)
        cache_hit_entry = {
            "id": 1,
            "question": "What is the inventory?",
            "conversation_context": "",
            "sql_query": "SELECT COUNT(*) FROM inventory",
            "description": "Inventory count",
            "conversation_id": "conv-123",
            "created_at": cached_time,
            "message_id": None,
            "question_distance": 0.08,  # 1 - 0.92
            "context_distance": 0.10,
        }

        mock_pool.set_query_results(
            [
                None,  # 1. Dimension check SELECT
                None,  # 2. _index_exists for space_idx
                None,  # 3. _index_exists for question_embedding_idx
                None,  # 4. _index_exists for context_embedding_idx
                None,  # 5. _index_exists for unique_question_idx
                None,  # 6. _index_exists for prompt_history_conversation_idx
                None,  # 7. _index_exists for prompt_history_space_idx
                None,  # 8. _index_exists for prompt_history_unique_prompt_idx
                None,  # 9. _index_exists for prompt_history_cache_entry_idx
                None,  # 10. find_similar SELECT - miss
                cache_hit_entry,  # 11. find_similar SELECT on second call - hit!
            ]
        )

        context_aware_cache = PostgresContextAwareGenieService(
            impl=mock_genie_service,
            parameters=semantic_params,
            workspace_client=None,  # No context for this test
        ).initialize()

        lru_params = Mock(spec=GenieLRUCacheParametersModel)
        lru_params.warehouse = mock_warehouse
        lru_params.capacity = 100
        lru_params.time_to_live_seconds = 3600

        lru_cache = LRUCacheService(
            impl=context_aware_cache,
            parameters=lru_params,
        )

        # First call - store original question
        lru_cache.ask_question("What is the inventory?")
        assert mock_genie_service.call_count == 1

        # Second call with DIFFERENT but similar question
        # LRU misses (different question text), but context-aware cache hits
        result = lru_cache.ask_question_with_cache_info("Show me inventory count")

        # LRU propagates the context-aware cache hit status
        # No new Genie call was made (context-aware cache served it)
        assert result.cache_hit is True  # Propagated from context-aware cache hit
        assert (
            result.served_by == "PostgresContextAwareGenieService"
        )  # Context-aware cache served it
        assert (
            mock_genie_service.call_count == 1
        )  # No new Genie call - context-aware hit!

        # Verify LRU now has this new question cached
        # (it learned from the context-aware cache hit)
        assert lru_cache.size == 2  # Both questions now in LRU

        # Third call with same paraphrased question - now hits LRU!
        result3 = lru_cache.ask_question_with_cache_info("Show me inventory count")
        assert result3.cache_hit is True
        assert result3.served_by == "LRUCacheService"

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_semantic_hit_then_stores_in_lru(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that context-aware cache hit result gets stored in LRU for next exact match."""
        from datetime import timezone

        mock_genie_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        semantic_params = Mock(spec=GenieContextAwareCacheParametersModel)
        semantic_params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()

        mock_statement_response = Mock()
        mock_statement_response.status.state = StatementState.SUCCEEDED
        mock_statement_response.result.data_array = [["value"]]
        mock_statement_response.manifest.schema.columns = [Mock(name="col")]
        mock_workspace_client.statement_execution.execute_statement.return_value = (
            mock_statement_response
        )

        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"
        semantic_params.warehouse = mock_warehouse
        semantic_params.time_to_live_seconds = 86400
        semantic_params.similarity_threshold = 0.85
        semantic_params.context_similarity_threshold = 0.80
        semantic_params.question_weight = 0.6
        semantic_params.context_weight = 0.4
        semantic_params.embedding_dims = 1024
        semantic_params.embedding_model = "databricks-gte-large-en"
        semantic_params.table_name = "test_cache"
        semantic_params.context_window_size = 3
        semantic_params.max_context_tokens = 2000
        semantic_params.prompt_history_table = "test_prompt_history"
        semantic_params.max_prompt_history_length = 50
        semantic_params.use_genie_api_for_history = False
        semantic_params.prompt_history_ttl_seconds = None
        semantic_params.ivfflat_lists = 100
        semantic_params.ivfflat_probes = 10
        semantic_params.ivfflat_candidates = 20

        cached_time = datetime.now(timezone.utc)

        # Context-aware cache has a similar entry
        # Query results order during initialization:
        # 1. dimension check - table doesn't exist (None)
        # 2-5. _index_exists for 4 main table indexes (None each)
        # 6-9. _index_exists for 4 prompt history table indexes (None each)
        # 10. find_similar query - returns cache hit
        mock_pool.set_query_results(
            [
                None,  # Dimension check - table doesn't exist
                None,  # _index_exists for space_idx
                None,  # _index_exists for question_embedding_idx
                None,  # _index_exists for context_embedding_idx
                None,  # _index_exists for unique_question_idx
                None,  # _index_exists for prompt_history_conversation_idx
                None,  # _index_exists for prompt_history_space_idx
                None,  # _index_exists for prompt_history_unique_prompt_idx
                None,  # _index_exists for prompt_history_cache_entry_idx
                {
                    "id": 1,
                    "question": "Original question",
                    "conversation_context": "",
                    "sql_query": "SELECT * FROM data",
                    "description": "Description",
                    "conversation_id": "conv-id",
                    "created_at": cached_time,
                    "message_id": None,
                    "question_distance": 0.05,  # 1 - 0.95
                    "context_distance": 0.05,
                },
            ]
        )

        context_aware_cache = PostgresContextAwareGenieService(
            impl=mock_genie_service,
            parameters=semantic_params,
            workspace_client=None,  # No context for this test
        ).initialize()

        lru_params = Mock(spec=GenieLRUCacheParametersModel)
        lru_params.warehouse = mock_warehouse
        lru_params.capacity = 100
        lru_params.time_to_live_seconds = 3600

        lru_cache = LRUCacheService(
            impl=context_aware_cache,
            parameters=lru_params,
        )

        # LRU is empty
        assert lru_cache.size == 0

        # First call - LRU miss, but context-aware cache has the data
        # LRU propagates the context-aware cache hit status
        result1 = lru_cache.ask_question_with_cache_info("Similar question here")
        assert result1.cache_hit is True  # Propagated from context-aware cache hit
        assert (
            result1.served_by == "PostgresContextAwareGenieService"
        )  # Context-aware cache served it

        # LRU should now have stored this result (learned from semantic)
        assert lru_cache.size == 1

        # Second call with same text - should hit LRU directly
        result2 = lru_cache.ask_question_with_cache_info("Similar question here")
        assert result2.cache_hit is True
        assert result2.served_by == "LRUCacheService"

    @patch("dao_ai.memory.postgres.PostgresPoolManager")
    @patch("databricks_langchain.DatabricksEmbeddings")
    def test_similarity_below_threshold_misses(
        self,
        mock_embeddings_class: Mock,
        mock_pool_manager: Mock,
    ) -> None:
        """Test that similarity below threshold is treated as cache miss."""
        from datetime import timezone

        mock_genie_service = MockGenieService()
        mock_pool = MockPostgresPool()

        mock_embeddings = MockEmbeddings()
        mock_embeddings_class.return_value = mock_embeddings
        mock_pool_manager.get_pool.return_value = mock_pool

        semantic_params = Mock(spec=GenieContextAwareCacheParametersModel)
        semantic_params.database = Mock()
        mock_warehouse = Mock()
        mock_workspace_client = Mock()
        mock_warehouse.workspace_client = mock_workspace_client
        mock_warehouse.warehouse_id = "test-warehouse"
        semantic_params.warehouse = mock_warehouse
        semantic_params.time_to_live_seconds = 86400
        semantic_params.similarity_threshold = 0.85
        semantic_params.context_similarity_threshold = 0.80
        semantic_params.question_weight = 0.6
        semantic_params.context_weight = 0.4
        semantic_params.embedding_dims = 1024
        semantic_params.embedding_model = "databricks-gte-large-en"
        semantic_params.table_name = "test_cache"
        semantic_params.context_window_size = 3
        semantic_params.max_context_tokens = 2000
        semantic_params.prompt_history_table = "test_prompt_history"
        semantic_params.max_prompt_history_length = 50
        semantic_params.use_genie_api_for_history = False
        semantic_params.prompt_history_ttl_seconds = None
        semantic_params.ivfflat_lists = 100
        semantic_params.ivfflat_probes = 10
        semantic_params.ivfflat_candidates = 20

        cached_time = datetime.now(timezone.utc)

        # Return entry with question similarity BELOW threshold
        mock_pool.set_query_results(
            [
                None,  # Dimension check - table doesn't exist
                {
                    "id": 1,
                    "question": "Unrelated question",
                    "conversation_context": "",
                    "sql_query": "SELECT * FROM other",
                    "description": "Description",
                    "conversation_id": "conv-id",
                    "created_at": cached_time,
                    "message_id": None,
                    "question_distance": 0.40,  # 1 - 0.60 = below threshold
                    "context_distance": 0.35,
                },
            ]
        )

        context_aware_cache = PostgresContextAwareGenieService(
            impl=mock_genie_service,
            parameters=semantic_params,
            workspace_client=None,  # No context for this test
        ).initialize()

        lru_params = Mock(spec=GenieLRUCacheParametersModel)
        lru_params.warehouse = mock_warehouse
        lru_params.capacity = 100
        lru_params.time_to_live_seconds = 3600

        lru_cache = LRUCacheService(
            impl=context_aware_cache,
            parameters=lru_params,
        )

        # Call should miss both caches (LRU empty, semantic below threshold)
        result = lru_cache.ask_question_with_cache_info("Very different question")

        assert result.cache_hit is False
        assert mock_genie_service.call_count == 1  # Had to call Genie
