"""Tests for directive functionality.

Directives are hard rules injected into prompts.
They are stored in the 'directives' table.
"""

import uuid

import pytest

from hindsight_api.engine.memory_engine import MemoryEngine


@pytest.fixture
async def memory_with_bank(memory: MemoryEngine, request_context):
    """Memory engine with a bank that has some data.

    Uses a unique bank_id to avoid conflicts between parallel tests.
    """
    # Use unique bank_id to avoid conflicts between parallel tests
    bank_id = f"test-directives-{uuid.uuid4().hex[:8]}"

    # Ensure bank exists
    await memory.get_bank_profile(bank_id, request_context=request_context)

    # Add some test data
    await memory.retain_batch_async(
        bank_id=bank_id,
        contents=[
            {"content": "The team has daily standups at 9am where everyone shares their progress."},
            {"content": "Alice is the frontend engineer and specializes in React."},
            {"content": "Bob is the backend engineer and owns the API services."},
        ],
        request_context=request_context,
    )

    # Wait for any background tasks from retain to complete
    await memory.wait_for_background_tasks()

    yield memory, bank_id

    # Cleanup
    await memory.delete_bank(bank_id, request_context=request_context)


class TestBankMission:
    """Test bank mission operations."""

    async def test_set_and_get_mission(self, memory: MemoryEngine, request_context):
        """Test setting and getting a bank's mission."""
        bank_id = f"test-mission-{uuid.uuid4().hex[:8]}"

        # Set mission
        result = await memory.set_bank_mission(
            bank_id=bank_id,
            mission="Track customer feedback",
            request_context=request_context,
        )

        assert result["bank_id"] == bank_id
        assert result["mission"] == "Track customer feedback"

        # Get mission via profile
        profile = await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
        assert profile["mission"] == "Track customer feedback"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)


class TestDirectives:
    """Test directive functionality."""

    async def test_create_directive(self, memory: MemoryEngine, request_context):
        """Test creating a directive."""
        bank_id = f"test-directive-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create a directive
        directive = await memory.create_directive(
            bank_id=bank_id,
            name="Competitor Policy",
            content="Never mention competitor product names directly. If asked about competitors, redirect to our features.",
            request_context=request_context,
        )

        assert directive["name"] == "Competitor Policy"
        assert "Never mention competitor" in directive["content"]
        assert directive["is_active"] is True
        assert directive["priority"] == 0

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_directive_crud(self, memory: MemoryEngine, request_context):
        """Test basic CRUD operations for directives."""
        bank_id = f"test-directive-crud-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create
        directive = await memory.create_directive(
            bank_id=bank_id,
            name="Test Directive",
            content="Follow this rule",
            request_context=request_context,
        )
        directive_id = directive["id"]

        # Read
        retrieved = await memory.get_directive(
            bank_id=bank_id,
            directive_id=directive_id,
            request_context=request_context,
        )
        assert retrieved is not None
        assert retrieved["name"] == "Test Directive"
        assert retrieved["content"] == "Follow this rule"

        # List
        directives = await memory.list_directives(
            bank_id=bank_id,
            request_context=request_context,
        )
        assert len(directives) == 1
        assert directives[0]["id"] == directive_id

        # Update
        updated = await memory.update_directive(
            bank_id=bank_id,
            directive_id=directive_id,
            content="Updated rule content",
            request_context=request_context,
        )
        assert updated["content"] == "Updated rule content"

        # Delete
        deleted = await memory.delete_directive(
            bank_id=bank_id,
            directive_id=directive_id,
            request_context=request_context,
        )
        assert deleted is True

        # Verify deletion
        retrieved_after = await memory.get_directive(
            bank_id=bank_id,
            directive_id=directive_id,
            request_context=request_context,
        )
        assert retrieved_after is None

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_directive_priority(self, memory: MemoryEngine, request_context):
        """Test that directive priority works correctly."""
        bank_id = f"test-directive-priority-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create directives with different priorities
        await memory.create_directive(
            bank_id=bank_id,
            name="Low Priority",
            content="Low priority rule",
            priority=1,
            request_context=request_context,
        )

        await memory.create_directive(
            bank_id=bank_id,
            name="High Priority",
            content="High priority rule",
            priority=10,
            request_context=request_context,
        )

        # List should order by priority (desc)
        directives = await memory.list_directives(
            bank_id=bank_id,
            request_context=request_context,
        )
        assert len(directives) == 2
        assert directives[0]["name"] == "High Priority"
        assert directives[1]["name"] == "Low Priority"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_directive_is_active(self, memory: MemoryEngine, request_context):
        """Test that inactive directives are filtered by default."""
        bank_id = f"test-directive-active-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create active and inactive directives
        await memory.create_directive(
            bank_id=bank_id,
            name="Active Rule",
            content="This is active",
            is_active=True,
            request_context=request_context,
        )

        await memory.create_directive(
            bank_id=bank_id,
            name="Inactive Rule",
            content="This is inactive",
            is_active=False,
            request_context=request_context,
        )

        # List active only (default)
        active_directives = await memory.list_directives(
            bank_id=bank_id,
            active_only=True,
            request_context=request_context,
        )
        assert len(active_directives) == 1
        assert active_directives[0]["name"] == "Active Rule"

        # List all
        all_directives = await memory.list_directives(
            bank_id=bank_id,
            active_only=False,
            request_context=request_context,
        )
        assert len(all_directives) == 2

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)


class TestDirectiveTags:
    """Test tags functionality for directives."""

    async def test_directive_with_tags(self, memory: MemoryEngine, request_context):
        """Test creating a directive with tags."""
        bank_id = f"test-directive-tags-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create a directive with tags
        directive = await memory.create_directive(
            bank_id=bank_id,
            name="Tagged Rule",
            content="Follow this rule",
            tags=["project-a", "team-x"],
            request_context=request_context,
        )

        assert directive["tags"] == ["project-a", "team-x"]

        # Retrieve and verify tags
        retrieved = await memory.get_directive(
            bank_id=bank_id,
            directive_id=directive["id"],
            request_context=request_context,
        )
        assert retrieved["tags"] == ["project-a", "team-x"]

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_list_directives_by_tags(self, memory: MemoryEngine, request_context):
        """Test listing directives filtered by tags."""
        bank_id = f"test-directive-tags-list-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create directives with different tags
        await memory.create_directive(
            bank_id=bank_id,
            name="Rule A",
            content="Rule for project A",
            tags=["project-a"],
            request_context=request_context,
        )

        await memory.create_directive(
            bank_id=bank_id,
            name="Rule B",
            content="Rule for project B",
            tags=["project-b"],
            request_context=request_context,
        )

        # List all
        all_directives = await memory.list_directives(
            bank_id=bank_id,
            request_context=request_context,
        )
        assert len(all_directives) == 2

        # Filter by project-a tag
        filtered = await memory.list_directives(
            bank_id=bank_id,
            tags=["project-a"],
            request_context=request_context,
        )
        assert len(filtered) == 1
        assert filtered[0]["name"] == "Rule A"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_list_all_directives_without_filter(self, memory: MemoryEngine, request_context):
        """Test that listing directives without tags returns ALL directives (both tagged and untagged)."""
        bank_id = f"test-directive-list-all-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create untagged directive
        await memory.create_directive(
            bank_id=bank_id,
            name="Untagged Directive",
            content="This has no tags",
            request_context=request_context,
        )

        # Create tagged directive
        await memory.create_directive(
            bank_id=bank_id,
            name="Tagged Directive",
            content="This has tags",
            tags=["project-x"],
            request_context=request_context,
        )

        # List ALL directives (no tag filter, isolation_mode defaults to False)
        all_directives = await memory.list_directives(
            bank_id=bank_id,
            request_context=request_context,
        )

        # Should return BOTH tagged and untagged directives
        assert len(all_directives) == 2
        directive_names = {d["name"] for d in all_directives}
        assert "Untagged Directive" in directive_names
        assert "Tagged Directive" in directive_names

        # Verify the tagged directive has its tags
        tagged = next(d for d in all_directives if d["name"] == "Tagged Directive")
        assert tagged["tags"] == ["project-x"]

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)


class TestReflect:
    """Test reflect endpoint."""

    async def test_reflect_basic(self, memory_with_bank, request_context):
        """Test basic reflect query works."""
        memory, bank_id = memory_with_bank

        # Run a reflect query
        result = await memory.reflect_async(
            bank_id=bank_id,
            query="Who are the team members?",
            request_context=request_context,
        )

        assert result.text is not None
        assert len(result.text) > 0


class TestDirectivesInReflect:
    """Test that directives are followed during reflect operations."""

    async def test_reflect_follows_language_directive(self, memory: MemoryEngine, request_context):
        """Test that reflect follows a directive to respond in a specific language."""
        bank_id = f"test-directive-reflect-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Add some content in English
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "Alice is a software engineer who works at Google."},
                {"content": "Alice enjoys hiking on weekends and has been to Yosemite."},
                {"content": "Alice is currently working on a machine learning project."},
            ],
            request_context=request_context,
        )
        await memory.wait_for_background_tasks()

        # Create a directive to always respond in French
        await memory.create_directive(
            bank_id=bank_id,
            name="Language Policy",
            content="ALWAYS respond in French language. Never respond in English.",
            request_context=request_context,
        )

        # Check that the response contains French words/patterns
        # Common French words that would appear when talking about someone's job
        french_indicators = [
            "elle",
            "travaille",
            "une",
            "qui",
            "chez",
            "logiciel",
            "ingénieur",
            "ingénieure",
            "développeur",
            "développeuse",
            "ingénierie",
            "française",
        ]

        # Run reflect query (retry once since small LLMs may not always follow language directives)
        french_word_count = 0
        for _attempt in range(2):
            result = await memory.reflect_async(
                bank_id=bank_id,
                query="What does Alice do for work?",
                request_context=request_context,
            )
            assert result.text is not None
            assert len(result.text) > 0

            # At least some French words should appear in the response
            response_lower = result.text.lower()
            french_word_count = sum(1 for word in french_indicators if word in response_lower)
            if french_word_count >= 2:
                break

        assert (
            french_word_count >= 2
        ), f"Expected French response, but got: {result.text[:200]}"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_tagged_directive_not_applied_without_tags(self, memory: MemoryEngine, request_context):
        """Test that directives with tags are NOT applied to untagged reflect operations."""
        bank_id = f"test-directive-isolation-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Add some untagged content
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "The sky is blue."},
                {"content": "Water is wet."},
            ],
            request_context=request_context,
        )

        # Add some tagged content for the project-x context
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "The sky is blue according to project X standards.", "tags": ["project-x"]},
                {"content": "Project X color guidelines specify sky is blue.", "tags": ["project-x"]},
            ],
            request_context=request_context,
        )
        await memory.wait_for_background_tasks()

        # Create an untagged directive (should be applied)
        await memory.create_directive(
            bank_id=bank_id,
            name="General Policy",
            content="You MUST include the exact phrase 'MEMO-VERIFIED' somewhere in your response.",
            request_context=request_context,
        )

        # Create a tagged directive (should NOT be applied to untagged reflect)
        await memory.create_directive(
            bank_id=bank_id,
            name="Tagged Policy",
            content="You MUST include the exact phrase 'PROJECT-X-CLASSIFIED' somewhere in your response.",
            tags=["project-x"],
            request_context=request_context,
        )

        # Run reflect without tags - should only apply the untagged directive
        result = await memory.reflect_async(
            bank_id=bank_id,
            query="What color is the sky?",
            request_context=request_context,
        )

        # Verify the isolation mechanism: only untagged directive should be loaded
        untagged_directive_names = [d.name for d in result.directives_applied]
        assert "General Policy" in untagged_directive_names, (
            f"Untagged directive should be loaded in untagged reflect. Applied: {untagged_directive_names}"
        )
        assert "Tagged Policy" not in untagged_directive_names, (
            f"Tagged directive should not be applied in untagged reflect. Applied: {untagged_directive_names}"
        )

        # Now run reflect WITH the tag - should load BOTH directives
        result_tagged = await memory.reflect_async(
            bank_id=bank_id,
            query="What color is the sky?",
            tags=["project-x"],
            tags_match="all_strict",
            request_context=request_context,
        )

        # Verify the isolation mechanism: both directives should be loaded when tags match
        tagged_directive_names = [d.name for d in result_tagged.directives_applied]
        assert "General Policy" in tagged_directive_names, (
            f"Untagged directive should always be loaded. Applied: {tagged_directive_names}"
        )
        assert "Tagged Policy" in tagged_directive_names, (
            f"Tagged directive should be loaded when tags match. Applied: {tagged_directive_names}"
        )

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_reflect_based_on_structure(self, memory: MemoryEngine, request_context):
        """Test that reflect returns correct based_on structure with directives and memories separated."""
        bank_id = f"test-reflect-based-on-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Add some memories
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "Alice works at Google as a software engineer."},
                {"content": "Bob is a product manager at Microsoft."},
                {"content": "The team meets every Monday at 9am."},
            ],
            request_context=request_context,
        )
        await memory.wait_for_background_tasks()

        # Create a directive
        directive = await memory.create_directive(
            bank_id=bank_id,
            name="Professional Tone",
            content="Always maintain a professional and formal tone in responses.",
            request_context=request_context,
        )
        directive_id = directive["id"]

        # Run reflect which returns the core result
        result = await memory.reflect_async(
            bank_id=bank_id,
            query="Who works at Google?",
            request_context=request_context,
        )

        # Verify based_on structure exists
        assert result.based_on is not None

        # Verify directives key exists and contains our directive
        assert "directives" in result.based_on
        directives_list = result.based_on.get("directives", [])

        # Verify directives are dicts with id, name, content (not MemoryFact objects)
        assert len(directives_list) > 0, "Should have at least one directive"
        directive_found = False
        for d in directives_list:
            assert isinstance(d, dict), f"Directive should be dict, got {type(d)}"
            assert "id" in d, "Directive dict should have 'id'"
            assert "name" in d, "Directive dict should have 'name'"
            assert "content" in d, "Directive dict should have 'content'"
            # Check if this is our directive
            if d["id"] == directive_id:
                directive_found = True
                assert d["name"] == "Professional Tone"
                assert "professional" in d["content"].lower()

        assert directive_found, f"Our directive {directive_id} should be in based_on.directives"

        # Verify memories (world/experience) are separate from directives
        has_memories = "world" in result.based_on or "experience" in result.based_on
        assert has_memories, "Should have world or experience memories"

        # Verify that if mental-models key exists, it's separate from directives
        if "mental-models" in result.based_on:
            mental_models = result.based_on.get("mental-models", [])
            # Verify mental models are MemoryFact objects, not dicts like directives
            for mm in mental_models:
                assert hasattr(mm, "fact_type"), "Mental model should be MemoryFact with fact_type"
                assert mm.fact_type == "mental-models"
                assert hasattr(mm, "context")
                assert "mental model" in mm.context.lower()

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)


class TestDirectivesPromptInjection:
    """Test that directives are properly injected into the system prompt."""

    def test_build_directives_section_empty(self):
        """Test that empty directives returns empty string."""
        from hindsight_api.engine.reflect.prompts import build_directives_section

        result = build_directives_section([])
        assert result == ""

    def test_build_directives_section_with_content(self):
        """Test that directives with content are formatted correctly."""
        from hindsight_api.engine.reflect.prompts import build_directives_section

        directives = [
            {
                "name": "Competitor Policy",
                "content": "Never mention competitor names. Redirect to our features.",
            }
        ]

        result = build_directives_section(directives)

        assert "## DIRECTIVES (MANDATORY)" in result
        assert "Competitor Policy" in result
        assert "Never mention competitor names" in result
        assert "NEVER violate these directives" in result

    def test_system_prompt_includes_directives(self):
        """Test that build_system_prompt_for_tools includes directives."""
        from hindsight_api.engine.reflect.prompts import build_system_prompt_for_tools

        bank_profile = {"name": "Test Bank", "mission": "Test mission"}
        directives = [
            {
                "name": "Test Directive",
                "content": "Follow this rule",
            }
        ]

        prompt = build_system_prompt_for_tools(
            bank_profile=bank_profile,
            directives=directives,
        )

        assert "## DIRECTIVES (MANDATORY)" in prompt
        assert "Follow this rule" in prompt
        # Directives should appear before CRITICAL RULES
        directives_pos = prompt.find("## DIRECTIVES")
        critical_rules_pos = prompt.find("## CRITICAL RULES")
        assert directives_pos < critical_rules_pos


class TestMentalModelHistory:
    """Test mental model history persistence."""

    async def test_history_recorded_on_content_update(self, memory: MemoryEngine, request_context):
        """Test that updating content records a history entry."""
        bank_id = f"test-mm-history-{uuid.uuid4().hex[:8]}"
        await memory.get_bank_profile(bank_id, request_context=request_context)

        mm = await memory.create_mental_model(
            bank_id=bank_id,
            name="Test Model",
            source_query="What is the test?",
            content="Original content",
            request_context=request_context,
        )

        # No history yet
        history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
        assert history == []

        # Update content
        await memory.update_mental_model(
            bank_id=bank_id,
            mental_model_id=mm["id"],
            content="Updated content",
            request_context=request_context,
        )

        history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
        assert len(history) == 1
        assert history[0]["previous_content"] == "Original content"
        assert "changed_at" in history[0]

        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_history_ordered_most_recent_first(self, memory: MemoryEngine, request_context):
        """Test that history is returned most recent first."""
        bank_id = f"test-mm-history-order-{uuid.uuid4().hex[:8]}"
        await memory.get_bank_profile(bank_id, request_context=request_context)

        mm = await memory.create_mental_model(
            bank_id=bank_id,
            name="Test Model",
            source_query="What is the test?",
            content="v1",
            request_context=request_context,
        )

        await memory.update_mental_model(
            bank_id=bank_id,
            mental_model_id=mm["id"],
            content="v2",
            request_context=request_context,
        )
        await memory.update_mental_model(
            bank_id=bank_id,
            mental_model_id=mm["id"],
            content="v3",
            request_context=request_context,
        )

        history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
        assert len(history) == 2
        # Most recent first: second update recorded "v2" as previous, first recorded "v1"
        assert history[0]["previous_content"] == "v2"
        assert history[1]["previous_content"] == "v1"

        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_history_not_recorded_on_name_only_update(self, memory: MemoryEngine, request_context):
        """Test that updating only name does not record history."""
        bank_id = f"test-mm-history-name-{uuid.uuid4().hex[:8]}"
        await memory.get_bank_profile(bank_id, request_context=request_context)

        mm = await memory.create_mental_model(
            bank_id=bank_id,
            name="Original Name",
            source_query="What is the test?",
            content="Content",
            request_context=request_context,
        )

        await memory.update_mental_model(
            bank_id=bank_id,
            mental_model_id=mm["id"],
            name="Updated Name",
            request_context=request_context,
        )

        history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
        assert history == []

        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_history_returns_none_for_missing_model(self, memory: MemoryEngine, request_context):
        """Test that history returns None when mental model doesn't exist."""
        bank_id = f"test-mm-history-missing-{uuid.uuid4().hex[:8]}"
        await memory.get_bank_profile(bank_id, request_context=request_context)

        result = await memory.get_mental_model_history(
            bank_id, "nonexistent-id", request_context=request_context
        )
        assert result is None

        await memory.delete_bank(bank_id, request_context=request_context)


class TestMentalModelRefreshTagSecurity:
    """Test that mental model refresh respects tag-based security boundaries."""

    async def test_refresh_with_tags_only_accesses_same_tagged_models(
        self, memory: MemoryEngine, request_context
    ):
        """Test that refreshing a mental model with tags can only access other models with the same tags.

        This is a security test to ensure that mental models with tags (e.g., user:alice)
        cannot access mental models from other scopes (e.g., user:bob or no tags) during refresh.
        """
        bank_id = f"test-refresh-tags-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Add some facts with different tags
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "Alice works on the frontend React project. Alice's favorite color is blue.", "tags": ["user:alice"]},
                {"content": "Alice prefers working in the morning. Alice drinks coffee every day.", "tags": ["user:alice"]},
                {"content": "Bob works on the backend API services. Bob's favorite language is Python.", "tags": ["user:bob"]},
                {"content": "Bob prefers working at night. Bob drinks tea every day.", "tags": ["user:bob"]},
                {"content": "The company has 100 employees and is growing fast.", "tags": []},  # No tags
            ],
            request_context=request_context,
        )

        # Wait for background processing
        await memory.wait_for_background_tasks()

        # Create mental model for user:alice with sensitive data
        mm_alice = await memory.create_mental_model(
            bank_id=bank_id,
            name="Alice's Work Profile",
            source_query="What does Alice work on?",
            content="Alice is a frontend engineer specializing in React",
            tags=["user:alice"],
            request_context=request_context,
        )

        # Create mental model for user:bob with sensitive data
        mm_bob = await memory.create_mental_model(
            bank_id=bank_id,
            name="Bob's Work Profile",
            source_query="What does Bob work on?",
            content="Bob is a backend engineer specializing in Python",
            tags=["user:bob"],
            request_context=request_context,
        )

        # Create mental model with no tags (should not be accessible from tagged models)
        mm_untagged = await memory.create_mental_model(
            bank_id=bank_id,
            name="Company Info",
            source_query="What is the company info?",
            content="The company has 100 employees",
            request_context=request_context,
        )

        # Create a mental model for user:alice that will be refreshed
        mm_alice_refresh = await memory.create_mental_model(
            bank_id=bank_id,
            name="Alice's Summary",
            source_query="What are all the facts about work and preferences?",  # Broad query that should match all facts
            content="Initial content",
            tags=["user:alice"],
            request_context=request_context,
        )

        # Refresh Alice's mental model
        refreshed = await memory.refresh_mental_model(
            bank_id=bank_id,
            mental_model_id=mm_alice_refresh["id"],
            request_context=request_context,
        )

        # SECURITY CHECK: The refreshed content should ONLY include information from
        # memories/models tagged with user:alice, NOT from user:bob or untagged
        refreshed_content = refreshed["content"].lower()

        # Should include Alice's content (either from facts or mental models)
        assert "alice" in refreshed_content, \
            "Refreshed model should access memories/models with matching tags (user:alice)"

        # MUST NOT include Bob's content (security violation)
        # Use word boundary matching to avoid false positives (e.g., "team" contains "tea")
        import re
        def contains_word(text: str, word: str) -> bool:
            """Check if text contains word as a whole word (not substring)."""
            return bool(re.search(rf'\b{re.escape(word)}\b', text, re.IGNORECASE))

        assert not contains_word(refreshed_content, "bob") and \
               not contains_word(refreshed_content, "python") and \
               not contains_word(refreshed_content, "tea"), \
            f"SECURITY VIOLATION: Refreshed model accessed memories/models with different tags (user:bob). Content: {refreshed['content']}"

        # MUST NOT include untagged content (security violation)
        assert "100 employees" not in refreshed_content and "growing fast" not in refreshed_content, \
            f"SECURITY VIOLATION: Refreshed model accessed untagged memories/models. Content: {refreshed['content']}"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_consolidation_only_refreshes_matching_tagged_models(
        self, memory: MemoryEngine, request_context
    ):
        """Test that consolidation only triggers refresh for mental models with matching tags.

        This is a security test to ensure that when tagged memories are consolidated,
        only mental models with overlapping tags get refreshed, not all mental models.
        """
        bank_id = f"test-consolidation-refresh-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create mental models with different tags, all with refresh_after_consolidation=true
        mm_alice = await memory.create_mental_model(
            bank_id=bank_id,
            name="Alice's Model",
            source_query="What about Alice?",
            content="Initial Alice content",
            tags=["user:alice"],
            trigger={"refresh_after_consolidation": True},
            request_context=request_context,
        )

        mm_bob = await memory.create_mental_model(
            bank_id=bank_id,
            name="Bob's Model",
            source_query="What about Bob?",
            content="Initial Bob content",
            tags=["user:bob"],
            trigger={"refresh_after_consolidation": True},
            request_context=request_context,
        )

        mm_untagged = await memory.create_mental_model(
            bank_id=bank_id,
            name="Untagged Model",
            source_query="What about general stuff?",
            content="Initial untagged content",
            trigger={"refresh_after_consolidation": True},
            request_context=request_context,
        )

        # Record initial last_refreshed_at timestamps
        alice_initial = mm_alice["last_refreshed_at"]
        bob_initial = mm_bob["last_refreshed_at"]
        untagged_initial = mm_untagged["last_refreshed_at"]

        # Add memories with user:alice tags
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "Alice likes React", "tags": ["user:alice"]},
                {"content": "Alice drinks coffee", "tags": ["user:alice"]},
            ],
            request_context=request_context,
        )

        # Trigger consolidation manually (this should only refresh Alice's mental model)
        from hindsight_api.engine.consolidation.consolidator import run_consolidation_job

        result = await run_consolidation_job(
            memory_engine=memory,
            bank_id=bank_id,
            request_context=request_context,
        )

        # Wait for background refresh tasks to complete
        await memory.wait_for_background_tasks()

        # Check that mental models were refreshed appropriately
        mm_alice_after = await memory.get_mental_model(
            bank_id, mm_alice["id"], request_context=request_context
        )
        mm_bob_after = await memory.get_mental_model(
            bank_id, mm_bob["id"], request_context=request_context
        )
        mm_untagged_after = await memory.get_mental_model(
            bank_id, mm_untagged["id"], request_context=request_context
        )

        # SECURITY CHECK: Only Alice's mental model and untagged model should be refreshed
        # Alice's model should be refreshed (tags match)
        assert mm_alice_after["last_refreshed_at"] != alice_initial or mm_alice_after["content"] != mm_alice["content"], \
            "Alice's mental model should be refreshed when user:alice memories are consolidated"

        # Bob's model should NOT be refreshed (tags don't match)
        assert mm_bob_after["last_refreshed_at"] == bob_initial, \
            "SECURITY VIOLATION: Bob's mental model was refreshed even though user:bob memories were not consolidated"

        # Untagged model should be refreshed (untagged models are always refreshed)
        assert mm_untagged_after["last_refreshed_at"] != untagged_initial or mm_untagged_after["content"] != mm_untagged["content"], \
            "Untagged mental model should be refreshed after any consolidation"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)

    async def test_refresh_mental_model_with_directives(self, memory: MemoryEngine, request_context):
        """Test that refreshing a mental model with directives works correctly."""
        bank_id = f"test-refresh-directives-{uuid.uuid4().hex[:8]}"

        # Ensure bank exists
        await memory.get_bank_profile(bank_id, request_context=request_context)

        # Create a directive
        directive = await memory.create_directive(
            bank_id=bank_id,
            name="Response Style",
            content="Always be concise and professional",
            request_context=request_context,
        )

        # Create a concept mental model to refresh
        concept = await memory.create_mental_model(
            bank_id=bank_id,
            name="Team Info",
            source_query="Team information summary",
            content="Initial team information",
            request_context=request_context,
        )

        # Add some memories
        await memory.retain_batch_async(
            bank_id=bank_id,
            contents=[
                {"content": "Alice is the team lead and handles project planning."},
                {"content": "Bob is a senior engineer who mentors junior developers."},
            ],
            request_context=request_context,
        )

        # Wait for retain to complete
        await memory.wait_for_background_tasks()

        # Refresh the concept mental model (this should include directive in based_on)
        refreshed = await memory.refresh_mental_model(
            bank_id=bank_id,
            mental_model_id=concept["id"],
            request_context=request_context,
        )

        # Wait for background tasks to complete
        await memory.wait_for_background_tasks()

        # Verify the refresh completed without errors
        assert refreshed is not None
        assert refreshed["content"] is not None

        # Get the updated mental model
        updated = await memory.get_mental_model(bank_id, concept["id"], request_context=request_context)
        assert updated["content"] != "Initial team information"

        # Cleanup
        await memory.delete_bank(bank_id, request_context=request_context)
