"""Tests for the remember() method (Mem0-style per-turn extraction).

Tests: basic extraction, deduplication, CONTRADICT flow,
stub AI fallback, MCP tool definition, and input validation.
"""
import json
import uuid

import pytest

from conftest import SmallMockAIClient
from trustmem.extraction.pipeline import ExtractionPipeline
from trustmem.extraction.prompts import CONFLICT_SIMILARITY_THRESHOLD
from trustmem.handler.persistent import PersistentHandler
from trustmem.ztm.constants import DEFAULT_CONFIDENCE, CONFIDENCE_LEVELS


# ============================================================
# Mock AI Clients
# ============================================================


class RememberMockAIClient(SmallMockAIClient):
    """AI client that returns structured JSON for remember tests."""

    def llm_judge(self, prompt: str) -> str:
        # Stage 1: Extraction
        if "extract" in prompt.lower() or "atomic facts" in prompt.lower():
            return json.dumps({
                "facts": [
                    {"content": "user likes green tea", "type": "semantic", "confidence": "high"},
                    {"content": "meeting with Bob at 3pm tomorrow", "type": "episodic", "confidence": "high"},
                ],
                "entities": [
                    {"name": "Bob", "type": "person"},
                ],
                "relations": [
                    {"subject": "user", "predicate": "meets_with", "object": "Bob"},
                ],
            })
        # Stage 3: Conflict resolution
        if "compare" in prompt.lower() or "action" in prompt.lower():
            return json.dumps({
                "results": [
                    {"index": 0, "action": "ADD", "reason": "new information"}
                ]
            })
        return "no_contradiction"


class NoopMockAIClient(SmallMockAIClient):
    """AI client that returns NOOP for conflict resolution."""

    def llm_judge(self, prompt: str) -> str:
        if "extract" in prompt.lower() or "atomic facts" in prompt.lower():
            return json.dumps({
                "facts": [
                    {"content": "user likes green tea", "type": "semantic", "confidence": "high"},
                ],
                "entities": [],
                "relations": [],
            })
        if "compare" in prompt.lower() or "action" in prompt.lower():
            return json.dumps({
                "results": [
                    {"index": 0, "action": "NOOP", "reason": "identical to existing"}
                ]
            })
        return "no_contradiction"


class UpdateMockAIClient(SmallMockAIClient):
    """AI client that returns CONTRADICT for conflict resolution (formerly UPDATE)."""

    def llm_judge(self, prompt: str) -> str:
        if "extract" in prompt.lower() or "atomic facts" in prompt.lower():
            return json.dumps({
                "facts": [
                    {"content": "user now prefers black tea", "type": "semantic", "confidence": "high"},
                ],
                "entities": [],
                "relations": [],
            })
        if "compare" in prompt.lower() or "action" in prompt.lower():
            return json.dumps({
                "results": [
                    {"index": 0, "action": "CONTRADICT", "reason": "newer version of existing"}
                ]
            })
        return "no_contradiction"


class ContradictMockAIClient(SmallMockAIClient):
    """AI client that returns CONTRADICT for conflict resolution."""

    def llm_judge(self, prompt: str) -> str:
        if "extract" in prompt.lower() or "atomic facts" in prompt.lower():
            return json.dumps({
                "facts": [
                    {"content": "user hates green tea", "type": "semantic", "confidence": "high"},
                ],
                "entities": [],
                "relations": [],
            })
        if "compare" in prompt.lower() or "action" in prompt.lower():
            return json.dumps({
                "results": [
                    {"index": 0, "action": "CONTRADICT", "reason": "opposite preference"}
                ]
            })
        return "no_contradiction"


class EmptyMockAIClient(SmallMockAIClient):
    """AI client that extracts no facts."""

    def llm_judge(self, prompt: str) -> str:
        if "extract" in prompt.lower() or "atomic facts" in prompt.lower():
            return '{"facts": [], "entities": [], "relations": []}'
        return "no_contradiction"


# ============================================================
# Fixtures
# ============================================================


@pytest.fixture
def handler():
    ai = RememberMockAIClient()
    return PersistentHandler(database_url=":memory:", ai_client=ai)


@pytest.fixture
def handler_noop():
    ai = NoopMockAIClient()
    return PersistentHandler(database_url=":memory:", ai_client=ai)


@pytest.fixture
def handler_update():
    ai = UpdateMockAIClient()
    return PersistentHandler(database_url=":memory:", ai_client=ai)


@pytest.fixture
def handler_contradict():
    ai = ContradictMockAIClient()
    return PersistentHandler(database_url=":memory:", ai_client=ai)


@pytest.fixture
def handler_empty():
    ai = EmptyMockAIClient()
    return PersistentHandler(database_url=":memory:", ai_client=ai)


@pytest.fixture
def sample_messages():
    return [
        {"role": "user", "content": "I really like green tea. Also I have a meeting with Bob at 3pm tomorrow."},
        {"role": "assistant", "content": "Got it! I'll remember that. Anything else?"},
    ]


# ============================================================
# Test: Basic Remember
# ============================================================


class TestRememberBasic:
    """Basic remember() functionality."""

    def test_remember_extracts_facts(self, handler, sample_messages):
        result = handler.remember(
            user_id="test-agent",
            messages=sample_messages,
        )
        assert "extracted" in result
        assert "stats" in result
        assert result["stats"]["total_extracted"] > 0
        assert result["stats"]["added"] > 0

    def test_remember_no_conversation_dump(self, handler, sample_messages):
        """remember() should NOT save a conversation dump (unlike ingest_conversation)."""
        result = handler.remember(
            user_id="test-agent",
            messages=sample_messages,
        )
        # No conversation_id in result (ingest_conversation returns one)
        assert "conversation_id" not in result

        # Check that no episodic "conversation" memory was created
        memories = handler.list_memories(user_id="test-agent")
        mem_list = memories if isinstance(memories, list) else memories.get("memories", [])
        for m in mem_list:
            tags = m.get("tags", [])
            if isinstance(tags, str):
                import json as _json
                try:
                    tags = _json.loads(tags)
                except Exception:
                    tags = []
            assert "conversation" not in tags, "remember() should not create conversation dump"

    def test_remember_returns_entities(self, handler, sample_messages):
        result = handler.remember(user_id="test-agent", messages=sample_messages)
        assert "entities" in result
        assert len(result["entities"]) > 0
        assert result["entities"][0]["name"] == "Bob"

    def test_remember_returns_relations(self, handler, sample_messages):
        result = handler.remember(user_id="test-agent", messages=sample_messages)
        assert "relations" in result
        # Relations may be empty if entities don't match (mock returns "user" which
        # is not in extracted entities). Just verify the structure exists.
        assert isinstance(result["relations"], list)

    def test_remember_with_user_id(self, handler, sample_messages):
        result = handler.remember(
            messages=sample_messages,
            user_id="user-123",
            session_id="sess-456",
        )
        assert result["stats"]["added"] > 0

    def test_remember_stats_structure(self, handler, sample_messages):
        result = handler.remember(user_id="test-agent", messages=sample_messages)
        stats = result["stats"]
        assert "total_extracted" in stats
        assert "added" in stats
        assert "skipped" in stats
        assert stats["total_extracted"] == stats["added"] + stats["skipped"] + stats.get("contradicted", 0)


# ============================================================
# Test: Deduplication (NOOP)
# ============================================================


class TestRememberDedup:
    """Tests that duplicate facts are properly handled as NOOP."""

    def test_noop_skips_duplicate(self, handler_noop, sample_messages):
        # First: add a memory that will be "similar"
        handler_noop.add_memory(
            user_id="test-agent",
            content="user likes green tea",
        )

        result = handler_noop.remember(
            user_id="test-agent",
            messages=sample_messages,
        )
        assert result["stats"]["skipped"] > 0
        noop_facts = [e for e in result["extracted"] if e["event"] == "NOOP"]
        assert len(noop_facts) > 0


# ============================================================
# Test: UPDATE → CONTRADICT flow (UPDATE is now handled as CONTRADICT)
# ============================================================


class TestRememberUpdate:
    """Tests that formerly-UPDATE facts are now handled as CONTRADICT."""

    def test_update_creates_contradict(self, handler_update, sample_messages):
        # First: add a memory that will be "similar" and outdated
        handler_update.add_memory(
            user_id="test-agent",
            content="user likes green tea",
        )

        result = handler_update.remember(
            user_id="test-agent",
            messages=sample_messages,
        )
        # Now handled as CONTRADICT (both kept, trust determines ranking)
        assert result["stats"]["total_extracted"] > 0


# ============================================================
# Test: CONTRADICT flow
# ============================================================


class TestRememberContradict:
    """Tests that CONTRADICT facts create contradicts provenance edges."""

    def test_contradict_creates_provenance_edge(self, handler_contradict, sample_messages):
        """CONTRADICT should add memory AND record contradicts edge."""
        # First: add a memory that will be the "existing" one
        handler_contradict.add_memory(
            user_id="test-agent",
            content="user likes green tea",
        )

        result = handler_contradict.remember(
            user_id="test-agent",
            messages=sample_messages,
        )

        # Check that CONTRADICT event was recorded
        contradict_facts = [e for e in result["extracted"] if e["event"] == "CONTRADICT"]
        if contradict_facts:
            # Verify contradicts provenance edge exists in memory_provenance table
            new_mid = contradict_facts[0]["memory_id"]
            prov_rows = handler_contradict._db.get_provenance_by_child(new_mid)
            assert any(r.get("relationship") == "contradicts" for r in prov_rows)

    def test_contradict_stats(self, handler_contradict, sample_messages):
        """CONTRADICT should be tracked in stats."""
        handler_contradict.add_memory(
            user_id="test-agent",
            content="user likes green tea",
        )

        result = handler_contradict.remember(
            user_id="test-agent",
            messages=sample_messages,
        )
        stats = result["stats"]
        assert "contradicted" in stats


# ============================================================
# Test: Stub/Empty extraction
# ============================================================


class TestRememberEdgeCases:
    """Tests for edge cases: empty extraction, stub AI."""

    def test_empty_extraction_returns_zero(self, handler_empty, sample_messages):
        result = handler_empty.remember(user_id="test-agent", messages=sample_messages)
        assert result["stats"]["total_extracted"] == 0
        assert result["stats"]["added"] == 0
        assert len(result["extracted"]) == 0


# ============================================================
# Test: MCP Tool Definition
# ============================================================


# ============================================================
# Test: Constants
# ============================================================


class TestRememberConstants:
    """Tests for remember-related constants."""

    def test_confidence_levels(self):
        """CONFIDENCE_LEVELS should contain the expected values."""
        assert "high" in CONFIDENCE_LEVELS
        assert "medium" in CONFIDENCE_LEVELS
        assert "low" in CONFIDENCE_LEVELS

    def test_default_confidence_is_valid(self):
        """DEFAULT_CONFIDENCE must be one of the valid confidence levels."""
        assert DEFAULT_CONFIDENCE in CONFIDENCE_LEVELS

    def test_conflict_similarity_threshold_range(self):
        """CONFLICT_SIMILARITY_THRESHOLD should be between 0 and 1."""
        assert 0 < CONFLICT_SIMILARITY_THRESHOLD < 1
