# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for Memory Consolidation Pipeline v2 (Sprint 6)."""

import pytest

from familiar.core.memory_consolidation_v2 import (
    ConsolidationAction,
    ExtractedFact,
    _infer_importance,
    _infer_tags,
    _infer_title,
    _text_overlap,
    apply_consolidation,
    classify_fact,
    consolidate_exchange,
    extract_facts,
)
from familiar.core.memory_tree import MemoryEntry, MemoryTree


@pytest.fixture
def tree(tmp_path):
    return MemoryTree(db_path=tmp_path / "consol_test.db")


# ── Extraction ──


class TestExtractFacts:
    def test_extract_name(self):
        facts = extract_facts("My name is George", "Nice to meet you!")
        assert len(facts) >= 1
        assert any(f.title == "User Name" for f in facts)

    def test_extract_workplace(self):
        facts = extract_facts("I work at Acme Corp", "Interesting!")
        assert any(f.title == "User Workplace" for f in facts)

    def test_extract_location(self):
        facts = extract_facts("I live in Portland", "Great city!")
        assert any(f.title == "User Location" for f in facts)

    def test_extract_preference(self):
        facts = extract_facts("I like dark roast coffee", "Good taste!")
        assert any(f.title == "User Preference" for f in facts)

    def test_extract_dislike(self):
        facts = extract_facts("I hate cilantro", "Noted!")
        assert any(f.title == "User Dislike" for f in facts)

    def test_extract_nothing_trivial(self):
        facts = extract_facts("Hello", "Hi there!")
        assert len(facts) == 0

    def test_extract_deduplicates(self):
        facts = extract_facts("My name is George and my name is George", "Got it!")
        name_facts = [f for f in facts if f.title == "User Name"]
        assert len(name_facts) <= 1

    def test_extract_sets_source(self):
        facts = extract_facts("My name is George", "Hi!", "conv-42")
        assert facts[0].source == "conversation:conv-42"

    def test_extract_self_description(self):
        facts = extract_facts("I am a software engineer", "Cool!")
        assert any(f.title == "User Self-Description" for f in facts)

    def test_extract_call_me(self):
        facts = extract_facts("Call me Mel", "Sure thing!")
        assert any(f.title == "User Name" for f in facts)


# ── Title/Tag/Importance Inference ──


class TestInference:
    def test_infer_title_name(self):
        assert _infer_title("my name is George") == "User Name"

    def test_infer_title_work(self):
        assert _infer_title("i work at Acme") == "User Workplace"

    def test_infer_title_fallback(self):
        title = _infer_title("something totally unique and weird")
        assert len(title) > 0

    def test_infer_tags_preference(self):
        tags = _infer_tags("I prefer dark roast")
        assert "preference" in tags
        assert "auto-consolidated" in tags

    def test_infer_tags_user_info(self):
        tags = _infer_tags("my name is George")
        assert "user_info" in tags

    def test_infer_importance_user_info(self):
        assert _infer_importance(["auto-consolidated", "user_info"]) == 0.8

    def test_infer_importance_preference(self):
        assert _infer_importance(["auto-consolidated", "preference"]) == 0.6

    def test_infer_importance_default(self):
        assert _infer_importance(["auto-consolidated"]) == 0.5


# ── Similarity ──


class TestSimilarity:
    def test_identical(self):
        assert _text_overlap("hello world", "hello world") == 1.0

    def test_no_overlap(self):
        assert _text_overlap("alpha beta", "gamma delta") == 0.0

    def test_partial_overlap(self):
        score = _text_overlap("hello world foo", "hello world bar")
        assert 0.3 < score < 0.8

    def test_empty_string(self):
        assert _text_overlap("", "hello") == 0.0


# ── Classification ──


class TestClassifyFact:
    def _make_entry(self, title, body, memory_id="m1"):
        return MemoryEntry(
            memory_id=memory_id,
            tier="working",
            title=title,
            body=body,
            content_hash="x",
        )

    def test_add_no_existing(self):
        fact = ExtractedFact(title="User Name", body="George", tags=[])
        result = classify_fact(fact, [])
        assert result.action == ConsolidationAction.ADD

    def test_noop_identical(self):
        fact = ExtractedFact(title="User Name", body="George", tags=[])
        existing = [self._make_entry("User Name", "George")]
        result = classify_fact(fact, existing)
        assert result.action == ConsolidationAction.NOOP

    def test_update_same_title_different_body(self):
        fact = ExtractedFact(
            title="User Workplace",
            body="I recently joined NewCo as a senior engineer in Portland",
            tags=[],
        )
        existing = [
            self._make_entry(
                "User Workplace",
                "I work at OldCo doing frontend development in Seattle",
            )
        ]
        result = classify_fact(fact, existing)
        assert result.action == ConsolidationAction.UPDATE

    def test_add_low_similarity(self):
        fact = ExtractedFact(title="User Allergy", body="peanuts", tags=[])
        existing = [self._make_entry("User Workplace", "I work at Acme")]
        result = classify_fact(fact, existing)
        assert result.action == ConsolidationAction.ADD


# ── Apply ──


class TestApplyConsolidation:
    def test_apply_add(self, tree):
        from familiar.core.memory_consolidation_v2 import ConsolidationResult

        results = [
            ConsolidationResult(
                fact=ExtractedFact(
                    title="User Name",
                    body="George",
                    tags=["user_info"],
                    importance=0.8,
                    source="test",
                ),
                action=ConsolidationAction.ADD,
                confidence=0.9,
            )
        ]
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = apply_consolidation(results)
        assert stats["add"] == 1
        entries = tree.list_by_tier("working")
        assert len(entries) == 1
        assert entries[0].title == "User Name"

    def test_apply_noop(self, tree):
        from familiar.core.memory_consolidation_v2 import ConsolidationResult

        results = [
            ConsolidationResult(
                fact=ExtractedFact(title="X", body="Y", tags=[]),
                action=ConsolidationAction.NOOP,
                confidence=0.9,
            )
        ]
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = apply_consolidation(results)
        assert stats["noop"] == 1
        assert tree.list_by_tier("working") == []

    def test_apply_skips_low_confidence(self, tree):
        from familiar.core.memory_consolidation_v2 import ConsolidationResult

        results = [
            ConsolidationResult(
                fact=ExtractedFact(title="X", body="Y", tags=[]),
                action=ConsolidationAction.ADD,
                confidence=0.1,
            )
        ]
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = apply_consolidation(results, confidence_threshold=0.5)
        assert stats["skipped"] == 1

    def test_apply_update(self, tree):
        from familiar.core.memory_consolidation_v2 import ConsolidationResult

        # Seed existing
        entry = tree.store(
            title="User Workplace",
            body="OldCo",
            tier="working",
            tags=["professional"],
        )
        results = [
            ConsolidationResult(
                fact=ExtractedFact(
                    title="User Workplace",
                    body="NewCo",
                    tags=["professional"],
                    importance=0.7,
                ),
                action=ConsolidationAction.UPDATE,
                existing_id=entry.memory_id,
                confidence=0.8,
            )
        ]
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = apply_consolidation(results)
        assert stats["update"] == 1
        # Old should be invalidated
        old = tree.get(entry.memory_id)
        assert not old.is_valid()

    def test_apply_delete(self, tree):
        from familiar.core.memory_consolidation_v2 import ConsolidationResult

        entry = tree.store(title="Wrong Fact", body="incorrect", tier="working")
        results = [
            ConsolidationResult(
                fact=ExtractedFact(title="Wrong Fact", body="corrected", tags=[]),
                action=ConsolidationAction.DELETE,
                existing_id=entry.memory_id,
                confidence=0.9,
                reason="contradicted by user",
            )
        ]
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = apply_consolidation(results)
        assert stats["delete"] == 1
        assert not tree.get(entry.memory_id).is_valid()


# ── End-to-End ──


class TestConsolidateExchange:
    def test_full_pipeline(self, tree):
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = consolidate_exchange(
                user_message="My name is George and I work at FamiliarAI",
                assistant_response="Nice to meet you, George! I see you work at FamiliarAI.",
                user_id="default",
                conversation_id="conv-1",
            )
        assert stats["add"] >= 1
        entries = tree.list_by_tier("working")
        assert len(entries) >= 1

    def test_full_pipeline_no_facts(self, tree):
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats = consolidate_exchange(
                user_message="Hello",
                assistant_response="Hi!",
            )
        assert stats["add"] == 0

    def test_full_pipeline_dedup(self, tree):
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            stats1 = consolidate_exchange(
                user_message="My name is George",
                assistant_response="Got it, George!",
            )
            stats2 = consolidate_exchange(
                user_message="My name is George",
                assistant_response="I know, George!",
            )
        assert stats1["add"] >= 1
        # Second time should NOOP or UPDATE, not ADD again
        assert stats2["add"] == 0

    def test_full_pipeline_update(self, tree):
        with pytest.MonkeyPatch.context() as mp:
            mp.setattr(
                "familiar.core.memory_tree.get_memory_tree",
                lambda: tree,
            )
            consolidate_exchange(
                user_message="I work at OldCo doing frontend in Seattle",
                assistant_response="Noted!",
            )
            stats = consolidate_exchange(
                user_message="I work at BrandNewCorp as a backend engineer in Portland",
                assistant_response="Updated!",
            )
        # Should detect the workplace change and update or add
        total_changes = stats["update"] + stats["add"]
        assert total_changes >= 1
