"""
tests/test_selective_forgetting.py
Selective Forgetting — entity-level deletion for GDPR compliance.

Verifies:
- Memories found by entity_nodes backlink and content text match
- dry_run returns impact without deleting
- Memories, entity nodes, entity edges, vectors are deleted
- Audit trail records forget_entity event with counts only (no PII)
- user_id scoping limits deletion to one user
- Cascading: provenance edges are cleaned up automatically
- SDK forget() requires user_id or visibility (scope validation)
"""
import json

import pytest


@pytest.fixture
def handler(mock_ai):
    from trustmem.handler.persistent import PersistentHandler

    h = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
    h.reset_stores()
    return h


@pytest.fixture
def sdk(mock_ai):
    from trustmem import TrustMem
    from trustmem.handler.persistent import PersistentHandler

    h = PersistentHandler(database_url=":memory:", ai_client=mock_ai)
    h.reset_stores()
    return TrustMem._from_handler(h)


def _seed_entity_data(handler):
    """Seed test data with entity-linked memories."""
    m1 = handler.add_memory("Alice likes sushi", user_id="user-a")
    m2 = handler.add_memory("Alice works at Acme Corp", user_id="user-a")
    m3 = handler.add_memory("Bob likes pizza", user_id="user-a")  # unrelated

    # Create entity nodes linked to memories
    handler._db.put_entity({
        "entity_id": "ent-alice-1",
        "name": "Alice",
        "entity_type": "person",
        "user_id": "user-a",
        "source_memory_id": m1["memory_id"],
        "is_active": 1,
    })
    handler._db.put_entity({
        "entity_id": "ent-alice-2",
        "name": "Alice",
        "entity_type": "person",
        "user_id": "user-a",
        "source_memory_id": m2["memory_id"],
        "is_active": 1,
    })

    # Create entity edges
    handler._db.put_entity({
        "entity_id": "ent-acme",
        "name": "Acme Corp",
        "entity_type": "organization",
        "user_id": "user-a",
        "source_memory_id": m2["memory_id"],
        "is_active": 1,
    })
    handler._db.put_entity_edge({
        "edge_id": "edge-alice-acme",
        "subject_id": "ent-alice-1",
        "subject_name": "Alice",
        "predicate": "works_at",
        "object_id": "ent-acme",
        "object_name": "Acme Corp",
        "user_id": "user-a",
        "source_memory_id": m2["memory_id"],
        "is_active": 1,
    })

    return m1, m2, m3


# ── dry_run ─────────────────────────────────────────────────────


class TestDryRun:
    def test_dry_run_returns_impact(self, handler):
        m1, m2, m3 = _seed_entity_data(handler)

        result = handler.forget_entity("Alice", user_id="user-a", dry_run=True)

        assert result["dry_run"] is True
        assert result["deleted_memories"] == 2
        assert result["deleted_entity_nodes"] == 2
        assert result["deleted_entity_edges"] == 1

    def test_dry_run_does_not_delete(self, handler):
        _seed_entity_data(handler)

        handler.forget_entity("Alice", user_id="user-a", dry_run=True)

        # Data should still exist
        stats = handler.get_memory_stats(user_id="user-a")
        assert stats["total_memories"] == 3


# ── Actual deletion ─────────────────────────────────────────────


class TestForgetEntity:
    def test_deletes_matching_memories(self, handler):
        m1, m2, m3 = _seed_entity_data(handler)

        result = handler.forget_entity("Alice", user_id="user-a")

        assert result["dry_run"] is False
        assert result["deleted_memories"] == 2

        # Alice memories gone
        assert handler._db.get_memory(m1["memory_id"]) is None
        assert handler._db.get_memory(m2["memory_id"]) is None

        # Bob memory still exists
        assert handler._db.get_memory(m3["memory_id"]) is not None

    def test_deletes_entity_nodes(self, handler):
        _seed_entity_data(handler)

        result = handler.forget_entity("Alice", user_id="user-a")

        assert result["deleted_entity_nodes"] == 2
        nodes = handler._db.find_entity_nodes_by_name("Alice", user_id="user-a")
        assert len(nodes) == 0

    def test_deletes_entity_edges(self, handler):
        _seed_entity_data(handler)

        result = handler.forget_entity("Alice", user_id="user-a")

        assert result["deleted_entity_edges"] == 1
        edges = handler._db.find_entity_edges_by_name("Alice", user_id="user-a")
        assert len(edges) == 0

    def test_content_text_match(self, handler):
        """Memories found by content LIKE even without entity_nodes link."""
        handler.add_memory("Charlie mentioned Alice yesterday", user_id="test")

        result = handler.forget_entity("Alice", user_id="test")

        assert result["deleted_memories"] == 1

    def test_case_insensitive_entity_match(self, handler):
        """Entity name matching is case-insensitive."""
        m1 = handler.add_memory("data about ALICE", user_id="test")
        handler._db.put_entity({
            "entity_id": "ent-alice-ci",
            "name": "ALICE",
            "entity_type": "person",
            "user_id": "test",
            "source_memory_id": m1["memory_id"],
            "is_active": 1,
        })

        result = handler.forget_entity("alice", user_id="test")

        assert result["deleted_memories"] >= 1
        assert result["deleted_entity_nodes"] >= 1

    def test_no_match_returns_zero(self, handler):
        _seed_entity_data(handler)

        result = handler.forget_entity("NonExistent", user_id="user-a")

        assert result["deleted_memories"] == 0
        assert result["deleted_entity_nodes"] == 0
        assert result["deleted_entity_edges"] == 0


# ── user_id scoping ────────────────────────────────────────────


class TestUserScoping:
    def test_scoped_to_user(self, handler):
        """forget with user_id only deletes that user's data."""
        handler.add_memory("Alice fact from user-a", user_id="user-a")
        handler.add_memory("Alice fact from user-b", user_id="user-b")

        result = handler.forget_entity("Alice", user_id="user-a")

        assert result["deleted_memories"] == 1

        # user-b's memory still exists
        stats_b = handler.get_memory_stats(user_id="user-b")
        assert stats_b["total_memories"] == 1

    def test_org_shared_not_deleted_when_user_specified(self, handler):
        """forget with user_id does NOT delete org-shared (user_id=NULL) data."""
        handler.add_memory("Alice shared fact", user_id=None)  # org shared
        handler.add_memory("Alice user fact", user_id="user-a")

        result = handler.forget_entity("Alice", user_id="user-a")

        assert result["deleted_memories"] == 1  # only user-a's

        # org shared still exists
        shared = handler._db.find_memories_by_entity("Alice", user_id=None, strict=True)
        assert len(shared) == 1


# ── Audit trail ─────────────────────────────────────────────────


class TestForgetAudit:
    def test_audit_event_recorded(self, handler):
        _seed_entity_data(handler)

        handler.forget_entity("Alice", user_id="user-a", reason="gdpr_erasure")

        all_audit = handler._db._conn.execute(
            "SELECT * FROM memory_audit_trail WHERE event_type = 'forget_entity'"
        ).fetchall()
        assert len(all_audit) == 1

        audit = dict(all_audit[0])
        assert audit["triggered_by"] == "gdpr_erasure"
        assert audit["memory_id"].startswith("FORGET:")

    def test_audit_contains_no_pii(self, handler):
        _seed_entity_data(handler)

        handler.forget_entity("Alice", user_id="user-a", reason="gdpr_erasure")

        all_audit = handler._db._conn.execute(
            "SELECT * FROM memory_audit_trail WHERE event_type = 'forget_entity'"
        ).fetchall()
        audit = dict(all_audit[0])
        details = json.loads(audit["details"])

        # No personal data in details
        assert "Alice" not in json.dumps(details)
        assert "entity" not in details

        # Only counts
        assert "deleted_memories" in details
        assert "deleted_entity_nodes" in details
        assert "deleted_entity_edges" in details
        assert "deleted_vectors" in details

    def test_audit_default_reason(self, handler):
        handler.add_memory("Alice test", user_id="test")

        handler.forget_entity("Alice", user_id="test")

        all_audit = handler._db._conn.execute(
            "SELECT * FROM memory_audit_trail WHERE event_type = 'forget_entity'"
        ).fetchall()
        assert dict(all_audit[0])["triggered_by"] == "manual"


# ── Cascade ─────────────────────────────────────────────────────


class TestCascade:
    def test_provenance_edges_cascaded(self, handler):
        """Provenance edges are auto-deleted via FK CASCADE."""
        m1 = handler.add_memory("Alice original", user_id="test")
        m2 = handler.add_memory("Alice updated", user_id="test")

        handler._db.put_provenance({
            "provenance_id": "prov-test",
            "child_memory_id": m2["memory_id"],
            "parent_memory_id": m1["memory_id"],
            "user_id": "test",
            "relationship": "contradicts",
        })

        handler.forget_entity("Alice", user_id="test")

        # Provenance edges should be gone (CASCADE)
        prov = handler._db.get_provenance_by_child(m2["memory_id"])
        assert len(prov) == 0


# ── SDK layer ───────────────────────────────────────────────────


class TestSDKForget:
    def test_sdk_forget_dry_run(self, sdk):
        sdk._handler.add_memory("Alice likes tea", user_id="default")

        result = sdk.forget("Alice", user_id="default", dry_run=True)

        assert result["dry_run"] is True
        assert result["deleted_memories"] == 1

    def test_sdk_forget_executes(self, sdk):
        sdk._handler.add_memory("Alice likes tea", user_id="default")

        result = sdk.forget("Alice", user_id="default", reason="gdpr_erasure")

        assert result["dry_run"] is False
        assert result["deleted_memories"] == 1

    def test_sdk_forget_with_user_id(self, sdk):
        sdk._handler.add_memory("Alice data", user_id="user-x")
        sdk._handler.add_memory("Alice data", user_id="user-y")

        result = sdk.forget("Alice", user_id="user-x")

        assert result["deleted_memories"] == 1

    def test_sdk_forget_requires_scope(self, sdk):
        """forget() without user_id or visibility raises ValueError."""
        sdk._handler.add_memory("Alice data", user_id="test")

        with pytest.raises(ValueError, match="user_id or visibility"):
            sdk.forget("Alice")

    def test_sdk_forget_with_visibility(self, sdk):
        """forget() with visibility='organization' deletes org-shared."""
        sdk._handler.add_memory("Alice shared", user_id=None)

        result = sdk.forget("Alice", visibility="organization")

        assert result["deleted_memories"] == 1
