"""Tests for TrustMem Python SDK."""
import pytest

from trustmem.sdk import TrustMem
from trustmem.handler.persistent import PersistentHandler


@pytest.fixture
def m(mock_ai):
    """TrustMem SDK with in-memory stores."""
    handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
    return TrustMem._from_handler(handler)


# ==============================================================
# Init
# ==============================================================
class TestInit:
    def test_init_no_api_key_raises(self, monkeypatch):
        from trustmem.exceptions import ConfigurationError
        monkeypatch.delenv("OPENAI_API_KEY", raising=False)
        # Block dotenv from re-loading .env
        import unittest.mock
        with unittest.mock.patch.dict("sys.modules", {"dotenv": None}):
            with pytest.raises(ConfigurationError, match="OPENAI_API_KEY"):
                TrustMem()

    def test_from_handler(self, mock_ai):
        handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
        m = TrustMem._from_handler(handler)
        assert m._handler is handler

    def test_from_handler_custom(self, mock_ai):
        handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
        m = TrustMem._from_handler(handler)
        assert m._handler is handler

    def test_repr(self, m):
        r = repr(m)
        assert "TrustMem(" in r

    def test_context_manager(self, mock_ai):
        handler = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
        with TrustMem._from_handler(handler) as tm:
            assert isinstance(tm, TrustMem)


# ==============================================================
# add
# ==============================================================
class TestAdd:
    def test_add_messages(self, m):
        messages = [
            {"role": "user", "content": "I like Python programming"},
            {"role": "assistant", "content": "Noted!"},
        ]
        result = m.add(messages, user_id="user1")
        assert "stats" in result

    def test_add_with_user_id(self, m):
        messages = [{"role": "user", "content": "Bob's data"}]
        result = m.add(messages, user_id="bob")
        assert "stats" in result

    def test_add_with_tags(self, m):
        messages = [{"role": "user", "content": "Tagged"}]
        result = m.add(messages, user_id="user1", tags=["food", "preference"])
        assert "stats" in result

    def test_add_string_raises_validation_error(self, m):
        from trustmem.exceptions import ValidationError
        with pytest.raises(ValidationError, match="list\\[dict\\]"):
            m.add("Hello world")

    def test_add_invalid_type_raises(self, m):
        from trustmem.exceptions import ValidationError
        with pytest.raises(ValidationError, match="list\\[dict\\]"):
            m.add(123)


# ==============================================================
# search
# ==============================================================
class TestSearch:
    def test_search_returns_dict_with_results_and_relations(self, m):
        m._handler.add_memory("The sky is blue")
        result = m.search("sky color", visibility="organization")
        assert isinstance(result, dict)
        assert "results" in result
        assert "relations" in result
        assert isinstance(result["results"], list)
        assert isinstance(result["relations"], list)

    def test_search_empty(self, m):
        result = m.search("nonexistent topic", visibility="organization")
        assert result["results"] == []
        assert result["relations"] == []

    def test_search_finds_memory(self, m):
        m._handler.add_memory("Python is a great language")
        result = m.search("Python", visibility="organization")
        assert isinstance(result["results"], list)

    def test_search_includes_relations(self, m):
        """When entity matches query, relations are included."""
        m._handler.add_entity_edge(
            subject_name="Python",
            subject_type="technology",
            predicate="is",
            object_name="programming language",
            object_type="concept",
        )
        result = m.search("Python", visibility="organization")
        assert len(result["relations"]) >= 1

    def test_search_result_fields(self, m):
        """Search results contain expected ZTM v2 fields."""
        m._handler.add_memory("The sky is blue")
        result = m.search("sky color", visibility="organization")
        if result["results"]:
            r = result["results"][0]
            assert "memory_id" in r
            assert "content" in r
            assert "similarity" in r
            assert "confidence" in r
            assert "tags" in r
            assert "created_at" in r
            assert "last_evaluated_at" in r
            assert "freshness" in r

    def test_search_no_min_zone_param(self, m):
        """search() does not accept min_zone parameter."""
        with pytest.raises(TypeError):
            m.search("query", user_id="u1", min_zone=2)

    def test_search_no_include_trust_info_param(self, m):
        """search() does not accept include_trust_info parameter."""
        with pytest.raises(TypeError):
            m.search("query", user_id="u1", include_trust_info=True)


# ==============================================================
# get_all
# ==============================================================
class TestGetAll:
    def test_get_all_returns_dict_with_results_and_relations(self, m):
        result = m.get_all(visibility="organization")
        assert isinstance(result, dict)
        assert "results" in result
        assert "relations" in result

    def test_get_all_empty(self, m):
        result = m.get_all(visibility="organization")
        assert result["results"] == []
        assert result["relations"] == []

    def test_get_all_after_add(self, m):
        m._handler.add_memory("First memory")
        m._handler.add_memory("Second memory")
        result = m.get_all(visibility="organization")
        assert len(result["results"]) == 2

    def test_get_all_includes_relations(self, m):
        m._handler.add_memory("Some memory")
        m._handler.add_entity_edge(
            subject_name="Alex",
            subject_type="person",
            predicate="likes",
            object_name="sushi",
            object_type="food",
        )
        result = m.get_all(visibility="organization")
        assert len(result["results"]) == 1
        assert len(result["relations"]) >= 1

    def test_get_all_pagination(self, m):
        for i in range(5):
            m._handler.add_memory(f"Memory {i}")
        page = m.get_all(visibility="organization", limit=2, offset=0)
        assert len(page["results"]) == 2


# ==============================================================
# delete
# ==============================================================
class TestDelete:
    def test_delete_existing(self, m):
        result = m._handler.add_memory("To be deleted")
        deleted = m.delete(result["memory_id"])
        assert deleted is True

    def test_delete_nonexistent(self, m):
        import pytest
        with pytest.raises(Exception):
            m.delete("nonexistent-id")


# ==============================================================
# Scope resolution (_resolve_scope)
# ==============================================================
class TestResolveScope:
    def test_user_id_only(self, m):
        """user_id provided, visibility not — returns user_id."""
        result = m._resolve_scope("user1", None)
        assert result == "user1"

    def test_visibility_organization(self, m):
        """visibility='organization', user_id not — returns None."""
        result = m._resolve_scope(None, "organization")
        assert result is None

    def test_both_raises(self, m):
        """Both user_id and visibility raises ValueError."""
        with pytest.raises(ValueError, match="mutually exclusive"):
            m._resolve_scope("user1", "organization")

    def test_neither_raises(self, m):
        """Neither user_id nor visibility raises ValueError."""
        with pytest.raises(ValueError, match="required"):
            m._resolve_scope(None, None)

    def test_invalid_visibility_raises(self, m):
        """visibility other than 'organization' raises ValueError."""
        with pytest.raises(ValueError, match="must be 'organization'"):
            m._resolve_scope(None, "agent")

    def test_add_no_user_id_no_visibility_raises(self, m):
        """add() with no user_id and no visibility raises ValueError."""
        messages = [{"role": "user", "content": "test"}]
        with pytest.raises(ValueError):
            m.add(messages)

    def test_add_both_user_id_and_visibility_raises(self, m):
        """add() with both user_id and visibility raises ValueError."""
        messages = [{"role": "user", "content": "test"}]
        with pytest.raises(ValueError, match="mutually exclusive"):
            m.add(messages, user_id="u1", visibility="organization")

    def test_search_user_returns_user_memories_and_shared(self, m):
        """search with user_id returns user-scoped memories."""
        m._handler.add_memory("user fact", user_id="u1")
        m._handler.add_memory("shared fact")  # user_id=None (org-shared)
        result = m.search("fact", user_id="u1")
        contents = [r["content"] for r in result["results"]]
        assert "user fact" in contents

    def test_search_visibility_org_returns_shared_only(self, m):
        """search with visibility='organization' returns only shared (user_id=NULL)."""
        m._handler.add_memory("shared fact")  # user_id=None
        m._handler.add_memory("user fact", user_id="u1")
        result = m.search("fact", visibility="organization")
        contents = [r["content"] for r in result["results"]]
        assert "shared fact" in contents
        assert "user fact" not in contents


# ==============================================================
# Verify (ZTM)
# ==============================================================
class TestVerify:
    def test_verify_valid_memory(self, m):
        result = m._handler.add_memory("Verify me")
        v = m.verify(result["memory_id"])
        assert v["valid"] is True

    def test_verify_all_empty(self, m):
        v = m.verify_all()
        assert v["total"] == 0
        assert v["valid"] == 0

    def test_verify_all_after_add(self, m):
        m._handler.add_memory("First")
        m._handler.add_memory("Second")
        v = m.verify_all()
        assert v["total"] == 2
        assert v["valid"] == 2


# ==============================================================
# Audit Trail
# ==============================================================
class TestAuditTrail:
    def test_audit_trail_after_add(self, m):
        result = m._handler.add_memory("Tracked memory")
        trail = m.audit_trail(result["memory_id"])
        assert isinstance(trail, list)

    def test_audit_trail_nonexistent(self, m):
        trail = m.audit_trail("nonexistent-id")
        assert trail == []

    def test_record_audit_trail_signature(self, m):
        """record_audit_trail accepts new ZTM v2 signature."""
        result = m._handler.add_memory("Audit test")
        mid = result["memory_id"]
        entry = m._handler.record_audit_trail(
            mid,
            "test_event",
            triggered_by="test",
            detail={"key": "value"},
            old_confidence="medium",
            new_confidence="high",
        )
        assert entry["memory_id"] == mid
        assert entry["event_type"] == "test_event"
        assert entry["old_confidence"] == "medium"
        assert entry["new_confidence"] == "high"


# ==============================================================
# add_memory return value (ZTM v2)
# ==============================================================
class TestAddMemoryReturnValue:
    def test_returns_memory_id(self, m):
        result = m._handler.add_memory("Some fact")
        assert "memory_id" in result

    def test_returns_content_hash(self, m):
        result = m._handler.add_memory("Some fact")
        assert "content_hash" in result

    def test_returns_confidence(self, m):
        result = m._handler.add_memory("Some fact")
        assert "confidence" in result
        assert result["confidence"] in ("high", "medium", "low")

    def test_returns_embedding_status(self, m):
        result = m._handler.add_memory("Some fact")
        assert "embedding_status" in result

    def test_no_trust_score_in_return(self, m):
        """ZTM v2: add_memory must not return trust_score."""
        result = m._handler.add_memory("Some fact")
        assert "trust_score" not in result

    def test_no_zone_in_return(self, m):
        """ZTM v2: add_memory must not return zone."""
        result = m._handler.add_memory("Some fact")
        assert "zone" not in result

    def test_confidence_parameter(self, m):
        """_confidence parameter sets confidence level."""
        result = m._handler.add_memory(
            "High confidence fact",
            _confidence="high",
        )
        assert result["confidence"] == "high"

    def test_confidence_low(self, m):
        result = m._handler.add_memory(
            "Low confidence fact",
            _confidence="low",
        )
        assert result["confidence"] == "low"

    def test_confidence_defaults_to_medium(self, m):
        """Without _confidence, default is medium."""
        from trustmem.ztm.constants import DEFAULT_CONFIDENCE
        result = m._handler.add_memory("Default fact")
        assert result["confidence"] == DEFAULT_CONFIDENCE

    def test_no_initial_trust_score_param(self, m):
        """ZTM v2: _initial_trust_score parameter does not exist."""
        with pytest.raises(TypeError):
            m._handler.add_memory(
                "Fact",
                _initial_trust_score=0.80,
            )

    def test_no_initial_zone_param(self, m):
        """ZTM v2: _initial_zone parameter does not exist."""
        with pytest.raises(TypeError):
            m._handler.add_memory(
                "Fact",
                _initial_zone=4,
            )


# ==============================================================
# Screening Score (ZTM v2: confidence field)
# ==============================================================
class TestScreeningScore:
    def test_confidence_stored_via_commit_facts(self, m):
        """_commit_facts ADD stores confidence in the memory."""
        facts = [{"content": "Test fact for screening", "action": "ADD",
                  "confidence": "high"}]
        extracted, stats = m._handler._commit_facts(facts)
        assert stats["added"] == 1
        mid = extracted[0]["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert mem is not None
        assert mem.get("confidence") == "high"

    def test_confidence_medium_stored(self, m):
        """medium confidence is stored correctly."""
        facts = [{"content": "Medium confidence fact", "action": "ADD",
                  "confidence": "medium"}]
        extracted, stats = m._handler._commit_facts(facts)
        assert stats["added"] == 1
        mid = extracted[0]["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert mem.get("confidence") == "medium"

    def test_confidence_low_stored(self, m):
        """low confidence is stored correctly."""
        facts = [{"content": "Low confidence fact", "action": "ADD",
                  "confidence": "low"}]
        extracted, stats = m._handler._commit_facts(facts)
        assert stats["added"] == 1
        mid = extracted[0]["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert mem.get("confidence") == "low"


# ==============================================================
# Contradictions
# ==============================================================
class TestContradictions:
    def test_contradict_action_records_edge(self, m):
        """_commit_facts with CONTRADICT action creates contradicts provenance edge."""
        existing = m._handler.add_memory("Likes sushi")
        facts = [{
            "content": "Hates sushi",
            "type": "semantic",
            "confidence": "high",
            "action": "CONTRADICT",
            "conflict_with": existing["memory_id"],
            "reason": "opposite preference",
        }]
        extracted, stats = m._handler._commit_facts(facts)
        assert stats["contradicted"] == 1
        assert len(extracted) == 1
        assert extracted[0]["event"] == "CONTRADICT"
        new_mid = extracted[0]["memory_id"]
        # Verify contradicts provenance edge exists
        provenance = m._handler._db.get_provenance_by_child(new_mid)
        contradicts_edges = [p for p in provenance if p.get("relationship") == "contradicts"]
        assert len(contradicts_edges) >= 1
        assert contradicts_edges[0]["parent_memory_id"] == existing["memory_id"]

    def test_contradict_without_conflict_with_falls_to_add(self, m):
        """CONTRADICT without conflict_with should fall through to ADD."""
        facts = [{
            "content": "Some fact",
            "type": "semantic",
            "confidence": "high",
            "action": "CONTRADICT",
            "conflict_with": None,
            "reason": "",
        }]
        extracted, stats = m._handler._commit_facts(facts)
        assert stats["added"] == 1
        assert extracted[0]["event"] == "ADD"


# ==============================================================
# Knowledge Graph
# ==============================================================
class TestGraph:
    def test_add_relation(self, m):
        result = m.add_relation("Alice", "works_at", "Acme", visibility="organization")
        assert "edge_id" in result

    def test_search_relations(self, m):
        m.add_relation("Bob", "likes", "sushi", visibility="organization")
        results = m.search_relations("Bob", visibility="organization")
        assert isinstance(results, list)

    def test_get_graph(self, m):
        m.add_relation("Charlie", "knows", "Dave", visibility="organization")
        graph = m.get_graph("Charlie", visibility="organization")
        assert isinstance(graph, dict)
        assert "nodes" in graph
        assert "edges" in graph


# ==============================================================
# Statistics
# ==============================================================
class TestStats:
    def test_stats_returns_dict(self, m):
        m._handler.add_memory("Memory for stats")
        s = m.stats()
        assert isinstance(s, dict)

    def test_stats_has_confidence_distribution(self, m):
        """ZTM v2: stats returns confidence_distribution, not zone_distribution."""
        m._handler.add_memory("High confidence", _confidence="high")
        m._handler.add_memory("Medium confidence", _confidence="medium")
        s = m.stats()
        assert "confidence_distribution" in s

    def test_stats_no_zone_distribution(self, m):
        """ZTM v2: zone_distribution must not be in stats."""
        s = m.stats()
        assert "zone_distribution" not in s

    def test_entity_stats(self, m):
        s = m.entity_stats()
        assert isinstance(s, dict)


# ==============================================================
# Maintenance
# ==============================================================
class TestMaintenance:
    def test_backfill_hashes(self, m):
        result = m.backfill_hashes()
        assert isinstance(result, dict)

    def test_reset(self, m):
        m._handler.add_memory("To be cleared")
        m.reset()
        result = m.get_all(visibility="organization")
        assert result["results"] == []

    def test_reset_then_add(self, m):
        m.reset()
        m._handler.add_memory("After reset")
        result = m.get_all(visibility="organization")
        assert len(result["results"]) == 1

    def test_close_does_not_call_join_propagation_threads(self, m):
        """ZTM v2: close() no longer calls _join_propagation_threads."""
        assert not hasattr(m._handler, "_join_propagation_threads"), (
            "_join_propagation_threads should not exist in ZTM v2"
        )


# ==============================================================
# ZTM v2: removed features guard
# ==============================================================
class TestRemovedFeatures:
    def test_propagate_not_on_sdk(self, m):
        """m.propagate() is removed in ZTM v2."""
        assert not hasattr(m, "propagate"), "propagate() must be removed from SDK"

    def test_find_source_not_on_sdk(self, m):
        """m.find_source() is removed in ZTM v2."""
        assert not hasattr(m, "find_source"), "find_source() must be removed from SDK"

    def test_memory_has_no_is_contaminated(self, m):
        """ZTM v2: memories do not have is_contaminated field."""
        result = m._handler.add_memory("Clean memory")
        mid = result["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert "is_contaminated" not in mem

    def test_memory_has_no_trust_score(self, m):
        """ZTM v2: memories do not have trust_score field."""
        result = m._handler.add_memory("Some memory")
        mid = result["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert "trust_score" not in mem

    def test_memory_has_no_zone(self, m):
        """ZTM v2: memories do not have zone field."""
        result = m._handler.add_memory("Some memory")
        mid = result["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert "zone" not in mem

    def test_memory_has_no_corroboration_count(self, m):
        """ZTM v2: memories do not have corroboration_count field."""
        result = m._handler.add_memory("Some memory")
        mid = result["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert "corroboration_count" not in mem

    def test_memory_has_confidence(self, m):
        """ZTM v2: memories have confidence field (string)."""
        result = m._handler.add_memory("Some memory")
        mid = result["memory_id"]
        mem = m._handler._db.get_memory(mid)
        assert "confidence" in mem
        assert mem["confidence"] in ("high", "medium", "low")

    def test_ztm_constants_no_initial_trust_score(self):
        """ZTM v2: INITIAL_TRUST_SCORE is removed from ztm.constants."""
        import trustmem.ztm.constants as c
        assert not hasattr(c, "INITIAL_TRUST_SCORE"), (
            "INITIAL_TRUST_SCORE must be removed from ztm.constants in ZTM v2"
        )

    def test_ztm_constants_no_confidence_base_score(self):
        """ZTM v2: CONFIDENCE_BASE_SCORE is removed from ztm.constants."""
        import trustmem.ztm.constants as c
        assert not hasattr(c, "CONFIDENCE_BASE_SCORE"), (
            "CONFIDENCE_BASE_SCORE must be removed from ztm.constants in ZTM v2"
        )

    def test_ztm_constants_has_default_confidence(self):
        """ZTM v2: DEFAULT_CONFIDENCE exists in ztm.constants."""
        from trustmem.ztm.constants import DEFAULT_CONFIDENCE
        assert DEFAULT_CONFIDENCE in ("high", "medium", "low")


# ==============================================================
# Always Verify (read-time hash check)
# ==============================================================
class TestAlwaysVerify:
    """ZTM Always Verify: read-time content hash verification."""

    def test_search_returns_hash_verified_true(self, m):
        """Normal memory passes hash verification on search."""
        m._handler.add_memory("Valid memory")
        result = m.search("Valid", visibility="organization")
        for r in result["results"]:
            assert r["hash_verified"] is True
            assert r["stored_confidence"] == r["confidence"]

    def test_search_tampered_memory_demoted_to_low(self, m):
        """Tampered memory gets confidence demoted to low on search."""
        added = m._handler.add_memory("Original content", _confidence="high")
        mid = added["memory_id"]
        # Tamper: change content without updating hash
        m._handler._db._conn.execute(
            "UPDATE memories SET content = ? WHERE memory_id = ?",
            ("Tampered content", mid),
        )
        result = m.search("content", visibility="organization")
        tampered = [r for r in result["results"] if r["memory_id"] == mid]
        assert len(tampered) == 1
        assert tampered[0]["hash_verified"] is False
        assert tampered[0]["confidence"] == "low"
        assert tampered[0]["stored_confidence"] == "high"

    def test_get_returns_hash_verified(self, m):
        """get() returns hash_verified field."""
        added = m._handler.add_memory("Get me", user_id="test")
        mem = m.get(added["memory_id"])
        assert mem["hash_verified"] is True
        assert mem["stored_confidence"] == mem["confidence"]

    def test_get_tampered_memory_demoted(self, m):
        """Tampered memory detected on get()."""
        added = m._handler.add_memory("Safe content", _confidence="high", user_id="test")
        mid = added["memory_id"]
        m._handler._db._conn.execute(
            "UPDATE memories SET content = ? WHERE memory_id = ?",
            ("Hacked", mid),
        )
        mem = m.get(mid)
        assert mem["hash_verified"] is False
        assert mem["confidence"] == "low"
        assert mem["stored_confidence"] == "high"

    def test_get_all_returns_hash_verified(self, m):
        """get_all() returns hash_verified for each memory."""
        m._handler.add_memory("Memory A")
        m._handler.add_memory("Memory B")
        result = m.get_all(visibility="organization")
        for mem in result["results"]:
            assert "hash_verified" in mem
            assert "stored_confidence" in mem
            assert mem["hash_verified"] is True

    def test_get_all_tampered_memory_demoted(self, m):
        """Tampered memory detected in get_all()."""
        a = m._handler.add_memory("Good", _confidence="high")
        b = m._handler.add_memory("Also good", _confidence="medium")
        m._handler._db._conn.execute(
            "UPDATE memories SET content = ? WHERE memory_id = ?",
            ("Bad", a["memory_id"]),
        )
        result = m.get_all(visibility="organization")
        mems = {mem["memory_id"]: mem for mem in result["results"]}
        assert mems[a["memory_id"]]["hash_verified"] is False
        assert mems[a["memory_id"]]["confidence"] == "low"
        assert mems[b["memory_id"]]["hash_verified"] is True
        assert mems[b["memory_id"]]["confidence"] == "medium"

    def test_tampered_no_db_side_effect(self, m):
        """Always Verify does NOT modify stored confidence in DB."""
        added = m._handler.add_memory("Immutable", _confidence="high")
        mid = added["memory_id"]
        m._handler._db._conn.execute(
            "UPDATE memories SET content = ? WHERE memory_id = ?",
            ("Changed", mid),
        )
        # Read triggers demotion in return value
        mem = m.get(mid)
        assert mem["confidence"] == "low"
        # But DB still has original confidence
        raw = m._handler._db.get_memory(mid)
        assert raw["confidence"] == "high"

    def test_search_ranking_demotes_tampered(self, m):
        """Tampered memories rank lower due to effective confidence=low."""
        a = m._handler.add_memory("High trust fact", _confidence="high")
        b = m._handler.add_memory("Medium trust fact", _confidence="medium")
        # Tamper a
        m._handler._db._conn.execute(
            "UPDATE memories SET content = ? WHERE memory_id = ?",
            ("Tampered", a["memory_id"]),
        )
        result = m.search("fact", visibility="organization")
        if len(result["results"]) >= 2:
            ids = [r["memory_id"] for r in result["results"]]
            # b (medium, valid) should rank higher than a (low, tampered)
            assert ids.index(b["memory_id"]) < ids.index(a["memory_id"])


# ==============================================================
# Multi-tenant (user_id, visibility)
# ==============================================================
class TestMultiTenant:
    """Multi-tenant (user_id, visibility) tests."""

    def test_add_with_organization_and_user(self, m):
        """Memories can be added with user_id."""
        result = m.add(
            [{"role": "user", "content": "I like sushi"}],
            user_id="tanaka",
        )
        assert "stats" in result

    def test_search_user_scoped(self, m):
        """User-scoped: only same org+user sees the memory."""
        m.add_raw("user-scoped fact", user_id="user1")

        # Same user -> found
        r = m.search("user-scoped", user_id="user1")
        assert len(r["results"]) == 1

        # Different user -> not found
        r = m.search("user-scoped", user_id="user2")
        assert len(r["results"]) == 0

    def test_search_visibility_organization(self, m):
        """Organization visibility: org-shared fact visible."""
        m.add_raw("org-shared fact", visibility="organization")

        # visibility=organization -> found
        r = m.search("org-shared", visibility="organization")
        assert len(r["results"]) == 1

    def test_search_user_isolation(self, m):
        """user_id=None search only returns user_id IS NULL memories."""
        m.add_raw("shared knowledge", visibility="organization")  # user_id=None
        m.add_raw("user specific", user_id="user1")

        # visibility=organization -> only shared knowledge
        r = m.search("knowledge OR specific", visibility="organization")
        contents = [mem["content"] for mem in r["results"]]
        assert "shared knowledge" in contents
        assert "user specific" not in contents

    def test_get_all_user_scoped(self, m):
        """get_all respects user scoping."""
        m.add_raw("visible", user_id="u1")
        m.add_raw("other user", user_id="u2")

        r = m.get_all( user_id="u1")
        contents = [mem["content"] for mem in r["results"]]
        assert "visible" in contents
        assert "other user" not in contents

    def test_list_all_no_visibility_filter(self, m):
        """list_all ignores visibility (admin)."""
        m.add_raw("user-data", user_id="u1")
        m.add_raw("org-shared", visibility="organization")

        r = m.list_all()
        assert r["count"] >= 2

    def test_shared_knowledge(self, m):
        """Shared knowledge via visibility=organization."""
        m.add_raw("default org", visibility="organization")
        r = m.search("default org", visibility="organization")
        assert len(r["results"]) == 1

    def test_update_raw_inherits_org_user(self, m):
        """update_raw inherits user_id from old memory."""
        result = m.add_raw("old content", user_id="user1")
        mid = result["memory_id"]

        updated = m.update_raw(mid, "new content")
        new_mid = updated["new_memory_id"]

        new_mem = m.get(new_mid)
        # user_id inherited from old memory
        assert new_mem["user_id"] == "user1"


# ==============================================================
# memory_type
# ==============================================================
class TestMemoryType:
    def test_add_stores_semantic(self, m):
        """add() always stores memory_type='semantic'."""
        messages = [{"role": "user", "content": "I like sushi"}]
        m.add(messages, user_id="user1")
        result = m.get_all(user_id="user1")
        for mem in result["results"]:
            assert mem.get("memory_type") == "semantic"

    def test_add_raw_defaults_to_semantic(self, m):
        """add_raw() defaults to memory_type='semantic'."""
        result = m.add_raw("raw fact", user_id="user1")
        mid = result["memory_id"]
        mem = m.get(mid)
        assert mem["memory_type"] == "semantic"

    def test_add_raw_document_type(self, m):
        """add_raw(memory_type='document') stores correctly."""
        result = m.add_raw("doc chunk", memory_type="document", user_id="user1")
        mid = result["memory_id"]
        mem = m.get(mid)
        assert mem["memory_type"] == "document"

    def test_add_raw_invalid_type_raises(self, m):
        """add_raw() with invalid memory_type raises ValidationError."""
        from trustmem.exceptions import ValidationError
        with pytest.raises(ValidationError, match="memory_type"):
            m.add_raw("bad", memory_type="invalid", user_id="user1")

    def test_search_filter_semantic(self, m):
        """search(memory_type='semantic') filters correctly."""
        m.add_raw("semantic fact", memory_type="semantic", user_id="user1")
        m.add_raw("document chunk", memory_type="document", user_id="user1")
        result = m.search("fact", memory_type="semantic", user_id="user1")
        for r in result["results"]:
            mem = m.get(r["memory_id"])
            assert mem["memory_type"] == "semantic"

    def test_search_filter_document(self, m):
        """search(memory_type='document') filters correctly."""
        m.add_raw("semantic fact", memory_type="semantic", user_id="user1")
        m.add_raw("document chunk", memory_type="document", user_id="user1")
        result = m.search("chunk", memory_type="document", user_id="user1")
        for r in result["results"]:
            mem = m.get(r["memory_id"])
            assert mem["memory_type"] == "document"

    def test_search_no_type_returns_both(self, m):
        """search() without memory_type returns both types."""
        m.add_raw("semantic fact about cats", memory_type="semantic", user_id="user1")
        m.add_raw("document about cats", memory_type="document", user_id="user1")
        result = m.search("cats", user_id="user1")
        assert len(result["results"]) == 2

    def test_get_all_filter_by_type(self, m):
        """get_all(memory_type=...) filters correctly."""
        m.add_raw("sem1", memory_type="semantic", user_id="user1")
        m.add_raw("doc1", memory_type="document", user_id="user1")
        sem = m.get_all(memory_type="semantic", user_id="user1")
        doc = m.get_all(memory_type="document", user_id="user1")
        assert all(r["memory_type"] == "semantic" for r in sem["results"])
        assert all(r["memory_type"] == "document" for r in doc["results"])

    def test_document_excluded_from_contradict(self, mock_ai):
        """Document memories are excluded from CONTRADICT detection."""
        from tests.conftest import SmallMockAIClient
        from trustmem.handler.persistent import PersistentHandler

        # Use SmallMockAIClient for consistent embeddings
        ai = SmallMockAIClient(dim=8)
        handler = PersistentHandler(database_url=":memory:", ai_client=ai)
        sdk = TrustMem._from_handler(handler)

        # Add a document chunk
        sdk.add_raw("Alex likes sushi", memory_type="document", user_id="user1")

        # Now add via LLM pipeline — same content should not trigger CONTRADICT
        # because pipeline filters memory_type=semantic in _find_similar_with_vec
        from trustmem.extraction.pipeline import ExtractionPipeline
        pipeline = ExtractionPipeline(ai_client=ai, vectorstore=handler._vs)
        similar = pipeline._find_similar_with_vec(
            ai.embed("Alex likes sushi"), "user1",
        )
        # The document memory should be excluded from results
        assert len(similar) == 0
