"""
Tests for the core AuditChain — zero external dependencies.
"""
from __future__ import annotations

import hashlib
import threading
import time

import pytest

from merkle_audit.chain import AuditChain, ChainEntry


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

GENESIS_ROOT = "0" * 64


def fresh() -> AuditChain:
    return AuditChain()


# ---------------------------------------------------------------------------
# Basic append behaviour
# ---------------------------------------------------------------------------

class TestAppend:
    def test_initial_state(self):
        chain = fresh()
        assert chain.leaf_count() == 0
        assert chain.current_root() == GENESIS_ROOT

    def test_returns_chain_entry(self):
        chain = fresh()
        entry = chain.append("tool_executed", {"tool": "web_search"})
        assert isinstance(entry, ChainEntry)
        assert entry.leaf_index == 0
        assert entry.action_type == "tool_executed"

    def test_leaf_index_increments(self):
        chain = fresh()
        for i in range(5):
            e = chain.append(f"action_{i}", {})
            assert e.leaf_index == i
        assert chain.leaf_count() == 5

    def test_hashes_are_64_hex_chars(self):
        chain = fresh()
        e = chain.append("test", {"k": "v"})
        assert len(e.leaf_hash) == 64
        assert len(e.mmr_root) == 64
        assert len(e.payload_hash) == 64
        int(e.leaf_hash, 16)   # must be valid hex
        int(e.mmr_root, 16)
        int(e.payload_hash, 16)

    def test_root_changes_on_each_append(self):
        chain = fresh()
        roots = set()
        for _ in range(10):
            e = chain.append("event", {})
            roots.add(e.mmr_root)
        assert len(roots) == 10  # every root is unique

    def test_actor_stored(self):
        chain = fresh()
        e = chain.append("action", {}, actor="agent-007")
        assert e.actor == "agent-007"

    def test_agent_id_alias(self):
        chain = fresh()
        e = chain.append("action", {}, agent_id="legacy-agent")
        assert e.agent_id == "legacy-agent"
        assert e.actor == "legacy-agent"

    def test_payload_hash_is_deterministic(self):
        import json
        chain = fresh()
        payload = {"b": 2, "a": 1}
        e = chain.append("test", payload)
        expected = hashlib.sha256(
            json.dumps(payload, sort_keys=True, default=str).encode()
        ).hexdigest()
        assert e.payload_hash == expected

    def test_current_root_matches_last_entry(self):
        chain = fresh()
        for _ in range(3):
            e = chain.append("event", {})
        assert chain.current_root() == e.mmr_root


# ---------------------------------------------------------------------------
# Tamper detection
# ---------------------------------------------------------------------------

class TestTamperDetection:
    def test_valid_first_entry(self):
        chain = fresh()
        entry = chain.append("action", {"x": 1})
        assert chain.verify_entry(entry, GENESIS_ROOT)

    def test_valid_chain_of_entries(self):
        chain = fresh()
        prev_root = GENESIS_ROOT
        for i in range(5):
            entry = chain.append(f"action_{i}", {"i": i})
            assert chain.verify_entry(entry, prev_root), f"Entry {i} failed"
            prev_root = entry.mmr_root

    def test_tampered_action_type_detected(self):
        from dataclasses import replace
        chain = fresh()
        entry = chain.append("original", {})
        bad = replace(entry, action_type="tampered")
        assert not chain.verify_entry(bad, GENESIS_ROOT)

    def test_tampered_payload_hash_detected(self):
        from dataclasses import replace
        chain = fresh()
        entry = chain.append("action", {"data": "real"})
        bad = replace(entry, payload_hash="a" * 64)
        assert not chain.verify_entry(bad, GENESIS_ROOT)

    def test_tampered_leaf_hash_detected(self):
        from dataclasses import replace
        chain = fresh()
        entry = chain.append("action", {})
        bad = replace(entry, leaf_hash="b" * 64)
        assert not chain.verify_entry(bad, GENESIS_ROOT)

    def test_wrong_prev_root_detected(self):
        chain = fresh()
        entry = chain.append("action", {})
        assert not chain.verify_entry(entry, "f" * 64)

    def test_tampered_timestamp_detected(self):
        from dataclasses import replace
        chain = fresh()
        entry = chain.append("action", {})
        bad = replace(entry, created_at=entry.created_at + 1.0)
        assert not chain.verify_entry(bad, GENESIS_ROOT)


# ---------------------------------------------------------------------------
# Persist hook
# ---------------------------------------------------------------------------

class TestPersistHook:
    def test_hook_called_on_append(self):
        calls = []
        chain = fresh()
        chain.set_persist_hook(calls.append)
        chain.append("action", {})
        assert len(calls) == 1
        assert calls[0].action_type == "action"

    def test_hook_called_for_each_entry(self):
        calls = []
        chain = fresh()
        chain.set_persist_hook(calls.append)
        for _ in range(5):
            chain.append("event", {})
        assert len(calls) == 5

    def test_hook_failure_does_not_crash_chain(self):
        def broken_hook(entry):
            raise RuntimeError("DB down")

        chain = fresh()
        chain.set_persist_hook(broken_hook)
        entry = chain.append("action", {})  # must not raise
        assert entry.leaf_index == 0
        assert chain.leaf_count() == 1

    def test_hook_receives_correct_entry(self):
        received = []
        chain = fresh()
        chain.set_persist_hook(received.append)
        chain.append("payment", {"amount": 99}, actor="bank-agent")
        e = received[0]
        assert e.action_type == "payment"
        assert e.actor == "bank-agent"


# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------

class TestThreadSafety:
    def test_concurrent_appends_unique_indices(self):
        chain = fresh()
        results = []
        lock = threading.Lock()

        def worker():
            for _ in range(50):
                e = chain.append("event", {})
                with lock:
                    results.append(e.leaf_index)

        threads = [threading.Thread(target=worker) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert chain.leaf_count() == 500
        assert len(set(results)) == 500  # all leaf indices unique


# ---------------------------------------------------------------------------
# Module-level API
# ---------------------------------------------------------------------------

class TestModuleAPI:
    def test_log_event_returns_chain_entry(self):
        from merkle_audit.chain import log_event
        e = log_event("test_event", {"key": "value"})
        assert isinstance(e, ChainEntry)

    def test_get_chain_returns_singleton(self):
        from merkle_audit.chain import get_chain
        c1 = get_chain()
        c2 = get_chain()
        assert c1 is c2
