"""
tests/test_block_internal_injection.py — Unit tests for issue #866.

Block memory extraction on internal/system content.

Coverage:
  1. System messages are blocked by is_internal_content()
  2. Tool/engine output is blocked
  3. Normal user messages pass through
  4. Config toggle: extraction_block_internal=False allows all messages
  5. filter_internal_content_conversation() skips internal messages and counts them
  6. filter_internal_content_conversation() with block_internal=False returns unchanged
  7. WriteOpsMixin wires the filter (integration-level)
"""

from __future__ import annotations

import asyncio
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch, call

import pytest

from cortex.capture.wake_pipeline import (
    is_internal_content,
    filter_internal_content_conversation,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_config(**overrides):
    """Return a MagicMock mimicking CortexConfig with sensible defaults."""
    cfg = MagicMock()
    cfg.owner_id = "eva"
    cfg.extraction_enabled = True
    cfg.extraction_block_internal = True
    cfg.max_concurrent_extractions = 4
    cfg.max_llm_calls_per_session = 1000
    cfg.max_extraction_calls_per_batch = 25
    cfg.extract_assistant_turns = True
    cfg.coding_noise_filter = False
    cfg.known_subjects = []
    for k, v in overrides.items():
        setattr(cfg, k, v)
    return cfg


# ---------------------------------------------------------------------------
# 1. is_internal_content — blocked patterns
# ---------------------------------------------------------------------------

class TestIsInternalContent:
    """Unit tests for the is_internal_content() classifier."""

    @pytest.mark.parametrize("content,label", [
        (
            "Some context\n<relevant-memories>\n- Eva prefers cats\n</relevant-memories>",
            "already-injected memory block",
        ),
        (
            "[Internal task completion event] session_key: agent:main:subagent:abc123",
            "subagent completion report",
        ),
        (
            "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>\nresult content here",
            "child result begin wrapper",
        ),
        (
            "<<<END_UNTRUSTED_CHILD_RESULT>>>",
            "child result end wrapper",
        ),
        (
            "OpenClaw runtime context (internal): model=claude-opus session=abc",
            "runtime context block",
        ),
        (
            "## Inbound Context (trusted metadata)\nsender: eva\nchannel: telegram",
            "inbound context block",
        ),
        (
            "## Runtime context\nruntime: agent=main | host=lume",
            "runtime context block",
        ),
        (
            "## Runtime generated\nsome generated data",
            "runtime generated block",
        ),
        (
            "## Runtime metadata\nsome metadata",
            "runtime metadata block",
        ),
        (
            "session_key: agent:main:subagent:xyz type: subagent task",
            "session_key + subagent type",
        ),
        (
            "type: subagent_task some details here",
            "subagent_task type marker (underscore)",
        ),
        (
            "[[reply_to:12345]] Here is my response",
            "reply_to control tag",
        ),
        (
            "HEARTBEAT",
            "bare heartbeat",
        ),
        (
            "HEARTBEAT: session active at 2026-03-24T06:00:00Z",
            "heartbeat with detail",
        ),
        (
            "[Internal task started] processing request",
            "specific [Internal task] marker",
        ),
        (
            "[Internal context update] refreshing",
            "specific [Internal context] marker",
        ),
        (
            "[Internal system event] something happened",
            "specific [Internal system] marker",
        ),
        (
            "System: session started for user eva",
            "System: prefix",
        ),
        (
            "System: OpenClaw gateway initialized",
            "System: gateway init",
        ),
    ])
    def test_blocked_patterns(self, content: str, label: str) -> None:
        assert is_internal_content(content) is True, (
            f"Expected '{label}' to be blocked but is_internal_content() returned False"
        )

    @pytest.mark.parametrize("content,label", [
        (
            "I prefer dark mode for coding sessions",
            "normal user preference",
        ),
        (
            "Can you help me debug this Python function?",
            "user question",
        ),
        (
            "Andrew moved to Bangkok in 2022",
            "personal fact",
        ),
        (
            "The project uses FastAPI and SQLite",
            "tech context",
        ),
        (
            "Please remember that I'm vegetarian",
            "explicit memory request",
        ),
        (
            "Great, the deployment worked! Let's celebrate 🎉",
            "casual user message",
        ),
        (
            "My heartbeat is racing after the run",
            "user mentioning heartbeat (lowercase)",
        ),
        (
            "[Internal review] This document needs attention",
            "user writing [Internal review] — not a system marker",
        ),
        (
            "## Runtime\nHere is my runtime setup for the project",
            "user markdown heading '## Runtime' without context/generated/metadata",
        ),
        (
            "## Runtime analysis\nLooking at runtime performance",
            "user heading with 'Runtime' but not system-specific suffix",
        ),
        (
            "",
            "empty string",
        ),
        (
            None,  # type: ignore[arg-type]
            "None",
        ),
    ])
    def test_pass_through_content(self, content: Optional[str], label: str) -> None:
        assert is_internal_content(content) is False, (  # type: ignore[arg-type]
            f"Expected '{label}' to pass through but is_internal_content() returned True"
        )

    def test_heartbeat_case_sensitive(self) -> None:
        """HEARTBEAT pattern is case-sensitive — only uppercase matches."""
        assert is_internal_content("HEARTBEAT") is True
        assert is_internal_content("heartbeat") is False
        assert is_internal_content("HeartBeat") is False
        assert is_internal_content("My heartbeat is fast") is False

    def test_partial_match_in_longer_message(self) -> None:
        """Pattern matches anywhere in the message, not just at the start."""
        long_msg = (
            "The assistant said: Here is your context.\n"
            "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>\n"
            "Some result data\n"
        )
        assert is_internal_content(long_msg) is True

    def test_system_prefix_only_at_word_boundary(self) -> None:
        """'System:' must appear at start of string (anchored ^), with optional leading whitespace."""
        # "System:" at the beginning — blocked
        assert is_internal_content("System: hello") is True
        # Leading whitespace before "System:" — also blocked (B-IC4)
        assert is_internal_content("  System: hello") is True
        assert is_internal_content("\tSystem: hello") is True
        # "systematic" in the middle — should NOT be blocked (not "System:")
        assert is_internal_content("I prefer systematic approaches") is False


# ---------------------------------------------------------------------------
# 2. filter_internal_content_conversation
# ---------------------------------------------------------------------------

class TestFilterInternalContentConversation:
    """Tests for filter_internal_content_conversation()."""

    def test_filters_internal_messages(self) -> None:
        conversation = [
            {"role": "user", "content": "I love building with Python"},
            {"role": "system", "content": "HEARTBEAT: session alive"},
            {"role": "user", "content": "What's the capital of France?"},
            {"role": "assistant", "content": "[Internal task completion event] done"},
        ]
        filtered, skipped = filter_internal_content_conversation(conversation, block_internal=True)
        assert skipped == 2
        assert len(filtered) == 2
        assert filtered[0]["content"] == "I love building with Python"
        assert filtered[1]["content"] == "What's the capital of France?"

    def test_no_internal_messages_passthrough(self) -> None:
        conversation = [
            {"role": "user", "content": "Andrew lives in Bangkok"},
            {"role": "assistant", "content": "Got it, noted that Andrew is in Bangkok."},
        ]
        filtered, skipped = filter_internal_content_conversation(conversation, block_internal=True)
        assert skipped == 0
        assert len(filtered) == 2
        assert filtered == conversation

    def test_block_internal_false_returns_unchanged(self) -> None:
        """When block_internal=False, all messages pass through even if internal."""
        conversation = [
            {"role": "user", "content": "HEARTBEAT"},
            {"role": "system", "content": "System: init"},
            {"role": "user", "content": "Normal message"},
        ]
        filtered, skipped = filter_internal_content_conversation(conversation, block_internal=False)
        assert skipped == 0
        assert len(filtered) == 3
        assert filtered == conversation

    def test_non_dict_messages_preserved(self) -> None:
        """Non-dict items in the conversation are passed through unchanged."""
        conversation = [
            {"role": "user", "content": "Hello"},
            "raw string message",  # type: ignore[list-item]
            {"role": "assistant", "content": "HEARTBEAT"},
        ]
        filtered, skipped = filter_internal_content_conversation(conversation, block_internal=True)
        assert skipped == 1
        assert "raw string message" in filtered

    def test_empty_conversation(self) -> None:
        filtered, skipped = filter_internal_content_conversation([], block_internal=True)
        assert filtered == []
        assert skipped == 0

    def test_all_messages_internal(self) -> None:
        conversation = [
            {"role": "system", "content": "HEARTBEAT"},
            {"role": "user", "content": "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>"},
            {"role": "assistant", "content": "## Runtime context\nmodel=claude"},
        ]
        filtered, skipped = filter_internal_content_conversation(conversation, block_internal=True)
        assert skipped == 3
        assert filtered == []


# ---------------------------------------------------------------------------
# 3. Config toggle integration — filter is wired in write_ops
# ---------------------------------------------------------------------------

class TestWriteOpsFilterWiring:
    """Verify the filter is correctly imported and called from write_ops."""

    def test_filter_function_importable_from_write_ops(self) -> None:
        """filter_internal_content_conversation must be importable via write_ops."""
        import cortex.api.plugin.write_ops as wo
        assert hasattr(wo, "_filter_internal_content_conversation"), (
            "_filter_internal_content_conversation not imported in write_ops.py"
        )

    def test_filter_called_with_config_toggle(self) -> None:
        """When block_internal=True, filter removes internal messages.
        When block_internal=False, filter passes all messages through.
        This tests the filter function directly with the same config logic used
        in write_ops.remember()."""
        conversation = [
            {"role": "system", "content": "HEARTBEAT"},
            {"role": "user", "content": "[Internal task completion event]"},
            {"role": "user", "content": "Andrew lives in Bangkok"},
        ]

        # Simulate write_ops with block_internal=True
        cfg_on = _make_config(extraction_block_internal=True)
        block_on = getattr(cfg_on, "extraction_block_internal", True)
        filtered_on, skipped_on = filter_internal_content_conversation(
            conversation, block_internal=block_on
        )
        assert skipped_on == 2
        assert len(filtered_on) == 1
        assert filtered_on[0]["content"] == "Andrew lives in Bangkok"

        # Simulate write_ops with block_internal=False
        cfg_off = _make_config(extraction_block_internal=False)
        block_off = getattr(cfg_off, "extraction_block_internal", True)
        filtered_off, skipped_off = filter_internal_content_conversation(
            conversation, block_internal=block_off
        )
        assert skipped_off == 0
        assert len(filtered_off) == 3

    def test_write_ops_imports_filter(self) -> None:
        """The source code of write_ops.py must reference the filter."""
        import inspect
        import cortex.api.plugin.write_ops as wo
        src = inspect.getsource(wo)
        assert "_filter_internal_content_conversation" in src, (
            "write_ops.py does not call _filter_internal_content_conversation"
        )
        assert "extraction_block_internal" in src, (
            "write_ops.py does not check extraction_block_internal config"
        )
