"""Tests for tamper-proof audit trail (hash chain + append-only).

Covers:
- Hash chain creation and linking
- Chain verification (verify_audit_chain)
- Tamper detection (modified record, deleted record)
- Append-only enforcement (UPDATE/DELETE blocked by triggers)
- cleanup_audit_trail raises ValueError (not dry_run)
- clear_all resets chain properly
- Memory deletion does not cascade-delete audit records
- SDK-level verify_audit_chain
"""
import sqlite3
import uuid

import pytest

from trustmem.storage.sqlite_store import SQLiteStore, _compute_audit_hash, _AUDIT_HASH_GENESIS


@pytest.fixture
def store():
    """In-memory SQLiteStore for audit chain tests."""
    s = SQLiteStore(":memory:")
    return s


@pytest.fixture
def sdk(handler):
    """TrustMem SDK instance for integration tests."""
    from trustmem.sdk import TrustMem
    return TrustMem._from_handler(handler)


# ──────────────────────────────────────────────────────
# Hash chain creation
# ──────────────────────────────────────────────────────

class TestHashChainCreation:
    def test_first_record_has_genesis_prev_hash(self, store):
        entry = store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-1",
            "event_type": "memory_created",
            "created_at": "2026-01-01T00:00:00Z",
        })
        assert entry["prev_hash"] == _AUDIT_HASH_GENESIS
        assert entry["sequence_number"] == 1
        assert entry["record_hash"] is not None

    def test_chain_links_correctly(self, store):
        entries = []
        for i in range(3):
            entry = store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })
            entries.append(entry)

        assert entries[0]["prev_hash"] == _AUDIT_HASH_GENESIS
        assert entries[1]["prev_hash"] == entries[0]["record_hash"]
        assert entries[2]["prev_hash"] == entries[1]["record_hash"]
        assert entries[0]["sequence_number"] == 1
        assert entries[1]["sequence_number"] == 2
        assert entries[2]["sequence_number"] == 3

    def test_record_hash_is_deterministic(self, store):
        """Same inputs produce same hash."""
        h1 = _compute_audit_hash("m1", "created", None, "high", None, "{}", "2026-01-01", "GENESIS")
        h2 = _compute_audit_hash("m1", "created", None, "high", None, "{}", "2026-01-01", "GENESIS")
        assert h1 == h2

    def test_record_hash_changes_with_content(self, store):
        """Different inputs produce different hashes."""
        h1 = _compute_audit_hash("m1", "created", None, "high", None, "{}", "2026-01-01", "GENESIS")
        h2 = _compute_audit_hash("m1", "created", None, "low", None, "{}", "2026-01-01", "GENESIS")
        assert h1 != h2


# ──────────────────────────────────────────────────────
# Chain verification
# ──────────────────────────────────────────────────────

class TestChainVerification:
    def test_empty_chain_is_valid(self, store):
        result = store.verify_audit_chain()
        assert result["valid"] is True
        assert result["total_records"] == 0

    def test_valid_chain(self, store):
        for i in range(5):
            store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })
        result = store.verify_audit_chain()
        assert result["valid"] is True
        assert result["total_records"] == 5
        assert result["verified"] == 5

    def test_detects_tampered_record_hash(self, store):
        """Modifying record_hash is detected."""
        for i in range(3):
            store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })

        # Tamper with record #2 (bypass trigger)
        store._conn.execute("DROP TRIGGER IF EXISTS audit_trail_no_update")
        store._conn.execute(
            "UPDATE memory_audit_trail SET record_hash = 'tampered' "
            "WHERE sequence_number = 2"
        )
        store._conn.commit()

        result = store.verify_audit_chain()
        assert result["valid"] is False
        assert result["first_broken_at"] == 2
        assert "tampered" in result["error"]

    def test_detects_tampered_content(self, store):
        """Modifying event_type is detected via hash mismatch."""
        for i in range(3):
            store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })

        # Tamper with content of record #1 (bypass trigger)
        store._conn.execute("DROP TRIGGER IF EXISTS audit_trail_no_update")
        store._conn.execute(
            "UPDATE memory_audit_trail SET event_type = 'hacked' "
            "WHERE sequence_number = 1"
        )
        store._conn.commit()

        result = store.verify_audit_chain()
        assert result["valid"] is False
        assert result["first_broken_at"] == 1

    def test_detects_deleted_record(self, store):
        """Deleting a record breaks the chain."""
        for i in range(3):
            store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })

        # Delete record #2 (bypass trigger)
        store._conn.execute("DROP TRIGGER IF EXISTS audit_trail_no_delete")
        store._conn.execute(
            "DELETE FROM memory_audit_trail WHERE sequence_number = 2"
        )
        store._conn.commit()

        result = store.verify_audit_chain()
        assert result["valid"] is False
        # Should detect sequence gap (1 → 3)
        assert "gap" in result["error"].lower() or "mismatch" in result["error"].lower()


# ──────────────────────────────────────────────────────
# Append-only enforcement (triggers)
# ──────────────────────────────────────────────────────

class TestAppendOnly:
    def test_update_blocked_by_trigger(self, store):
        store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-1",
            "event_type": "memory_created",
            "created_at": "2026-01-01T00:00:00Z",
        })
        with pytest.raises(sqlite3.IntegrityError, match="prohibited"):
            store._conn.execute(
                "UPDATE memory_audit_trail SET event_type = 'hacked' "
                "WHERE sequence_number = 1"
            )

    def test_delete_blocked_by_trigger(self, store):
        store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-1",
            "event_type": "memory_created",
            "created_at": "2026-01-01T00:00:00Z",
        })
        with pytest.raises(sqlite3.IntegrityError, match="prohibited"):
            store._conn.execute(
                "DELETE FROM memory_audit_trail WHERE sequence_number = 1"
            )

    def test_insert_still_works(self, store):
        """INSERT (append) should succeed."""
        entry = store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-1",
            "event_type": "memory_created",
            "created_at": "2026-01-01T00:00:00Z",
        })
        assert entry["sequence_number"] == 1


# ──────────────────────────────────────────────────────
# cleanup_audit_trail (disabled)
# ──────────────────────────────────────────────────────

class TestCleanupDisabled:
    def test_dry_run_still_works(self, handler):
        handler.add_memory("some content", user_id="test")
        result = handler.cleanup_audit_trail("2099-01-01", dry_run=True)
        assert result["dry_run"] is True
        assert result["deleted"] >= 1

    def test_actual_delete_raises(self, handler):
        handler.add_memory("some content", user_id="test")
        with pytest.raises(ValueError, match="hash chain"):
            handler.cleanup_audit_trail("2099-01-01", dry_run=False)

    def test_sdk_cleanup_raises(self, sdk):
        sdk.add([{"role": "user", "content": "hello"}], user_id="test")
        with pytest.raises(ValueError, match="hash chain"):
            sdk.cleanup_audit_trail("2099-01-01", dry_run=False)


# ──────────────────────────────────────────────────────
# clear_all resets chain
# ──────────────────────────────────────────────────────

class TestClearAll:
    def test_clear_all_resets_chain(self, store):
        for i in range(3):
            store.put_audit_trail({
                "audit_id": str(uuid.uuid4()),
                "memory_id": f"mem-{i}",
                "event_type": "memory_created",
                "created_at": f"2026-01-0{i+1}T00:00:00Z",
            })

        store.clear_all()

        # Chain should be empty
        result = store.verify_audit_chain()
        assert result["valid"] is True
        assert result["total_records"] == 0

        # New records start fresh with GENESIS
        entry = store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-new",
            "event_type": "memory_created",
            "created_at": "2026-02-01T00:00:00Z",
        })
        assert entry["prev_hash"] == _AUDIT_HASH_GENESIS
        assert entry["sequence_number"] == 1

    def test_triggers_restored_after_clear(self, store):
        store.clear_all()

        # Trigger should be restored
        store.put_audit_trail({
            "audit_id": str(uuid.uuid4()),
            "memory_id": "mem-1",
            "event_type": "memory_created",
            "created_at": "2026-01-01T00:00:00Z",
        })
        with pytest.raises(sqlite3.IntegrityError, match="prohibited"):
            store._conn.execute(
                "DELETE FROM memory_audit_trail WHERE sequence_number = 1"
            )


# ──────────────────────────────────────────────────────
# Memory deletion does NOT cascade to audit trail
# ──────────────────────────────────────────────────────

class TestNoCascadeDelete:
    def test_audit_survives_memory_deletion(self, handler):
        result = handler.add_memory("important fact", user_id="test")
        memory_id = result["memory_id"]

        # Verify audit exists
        trail = handler.get_audit_trail(memory_id)
        assert len(trail) >= 1

        # Delete the memory
        handler.delete_memory(memory_id)

        # Audit record should still exist (no FK CASCADE)
        trail_after = handler._db.get_audit_trail(memory_id)
        assert len(trail_after) >= 1


# ──────────────────────────────────────────────────────
# SDK integration
# ──────────────────────────────────────────────────────

class TestSDKIntegration:
    def test_verify_audit_chain_via_sdk(self, handler):
        """Use handler.add_memory directly (MockAIClient's llm_judge returns no facts)."""
        from trustmem.sdk import TrustMem
        sdk = TrustMem._from_handler(handler)

        handler.add_memory("fact one", user_id="test")
        handler.add_memory("fact two", user_id="test")

        result = sdk.verify_audit_chain()
        assert result["valid"] is True
        assert result["total_records"] >= 2

    def test_audit_trail_includes_chain_fields(self, handler):
        from trustmem.sdk import TrustMem
        sdk = TrustMem._from_handler(handler)

        result = handler.add_memory("test content", user_id="test")
        mem_id = result["memory_id"]

        trail = sdk.audit_trail(mem_id)
        assert len(trail) >= 1
        record = trail[0]
        assert "record_hash" in record
        assert "prev_hash" in record
        assert "sequence_number" in record

    def test_multiple_operations_maintain_valid_chain(self, handler):
        from trustmem.sdk import TrustMem
        sdk = TrustMem._from_handler(handler)

        handler.add_memory("fact 1", user_id="test")
        handler.add_memory("fact 2", user_id="test")
        handler.add_memory("fact 3", user_id="test")

        result = sdk.verify_audit_chain()
        assert result["valid"] is True
        assert result["total_records"] >= 3
