"""Tests on Lab 16's script 03, Exercise 1 (recorded October 2026): an audit log of 376 entries,
8 employees x 47 questions, anchored every 50 entries, and the lab's tamper test (200 random
positions per kind of change, seed 16). The log here has the same length, the same askers in the
same order and the same anchors; its questions and answers are placeholders, which doesn't change
what any check catches. The full tamper test takes a few seconds in the browser.
Run: pytest test_audit_practice.py"""
from audit_practice import (GENESIS, Anchors, append, entry_hash, rehash, tamper_test, verify,
                            verify_plain)

USERS = ["ana", "jonas", "ravi", "helen", "marta", "erik", "olivia", "theo"]


def build():
    entries, anchors = [], Anchors(every=50)
    for i in range(376):
        append(entries, {"user": USERS[i % 8], "question": f"question {i // 8}", "answer": f"answer {i}"}, anchors)
    return entries, anchors


# Entry 0 of the lab's log, exactly as stored: Ana's question, the 4 documents she was shown, the answer.
ENTRY0_BODY = {
    "ts": "2026-10-20T09:00:00Z", "user": "ana", "groups": ["public", "staff", "support", "user:ana"],
    "question": "How long do customers have to return an item?",
    "policy": {"version": "policy-2026-10", "category": "none", "rule": None},
    "retrieval": {"mode": "pre-filter", "index_version": "723b0bddcd8275d4", "shown": [
        {"id": "pub-returns", "sha": "03e25881f6a16970", "score": 0.6241},
        {"id": "case-c104", "sha": "4fdca70333aabe1c", "score": 0.3673},
        {"id": "sup-escalation", "sha": "d1e0b9c3f2706320", "score": 0.3581},
        {"id": "pub-warranty", "sha": "7a4f50e941024e3a", "score": 0.3451}]},
    "model": {"requested": "gpt-5.4-nano", "returned": "gpt-5.4-nano-2026-03-17", "effort": "none",
              "prompt_version": "answer-v1"},
    "answer": "Customers can return unused items within **30 days of delivery** [pub-returns].",
}
ENTRY0_HASH = "9b7e34a5c7dfdb62391049b75afc041c6cfc2d76b044e7ca74903031ddbf2403"


def test_entry_zero_has_the_labs_fingerprint():
    assert entry_hash(GENESIS, 0, ENTRY0_BODY) == ENTRY0_HASH
    assert entry_hash(GENESIS, 0, dict(ENTRY0_BODY, answer="Customers can return items within 45 days.")) != ENTRY0_HASH


def test_an_intact_log_verifies_with_its_anchors():
    entries, anchors = build()
    assert verify(entries) is None and verify(entries, anchors) is None
    assert len(entries[0]["hash"]) == 64 and entries[0]["prev"] == GENESIS
    assert [m["seq"] for m in anchors.marks] == [49, 99, 149, 199, 249, 299, 349]
    assert all(entries[i]["prev"] == entries[i - 1]["hash"] for i in range(1, 376))


def test_numbers_miss_an_edit_the_chain_catches_it():
    entries, _ = build()
    assert entries[122]["body"]["user"] == "ravi"
    entries[122]["body"]["user"] = "olivia"          # "It says Olivia asked, not Ravi."
    assert verify_plain(entries) is None
    assert verify(entries) == "entry 122: content doesn't match its hash"


def test_numbers_and_chain_catch_a_delete_and_a_swap():
    entries, _ = build()
    deleted = entries[:122] + entries[123:]
    assert verify_plain(deleted) == "entry 122: sequence number 123"
    assert verify(deleted) == "entry 122: sequence number 123"
    swapped = entries[:122] + [entries[123], entries[122]] + entries[124:]
    assert verify_plain(swapped) == "entry 122: sequence number 123"


def test_rehash_is_a_new_list_and_leaves_the_log_alone():
    entries, _ = build()
    before = entries[200]["hash"]
    entries[122]["body"]["user"] = "olivia"
    covered = rehash(entries, 122)
    assert covered is not entries and entries[200]["hash"] == before
    assert covered[200]["hash"] != before
    assert covered[122]["prev"] == entries[121]["hash"] and covered[0] == entries[0]


def test_the_cover_up_fools_the_chain_but_not_the_anchors():
    entries, anchors = build()
    entries[122]["body"]["user"] = "olivia"
    covered = rehash(entries, 122)
    assert verify(covered) is None                    # "The check says: intact."
    assert verify(covered, anchors) == "anchor at 149: the log's hash differs from the anchored one"
    entries, _ = build()
    renumbered = rehash(entries[:122] + entries[123:], 122)
    assert verify_plain(renumbered) is None and verify(renumbered) is None
    assert verify(renumbered, anchors) == "anchor at 149: the log's hash differs from the anchored one"


def test_a_rewrite_after_the_last_anchor_goes_unnoticed():
    entries, anchors = build()
    entries[360]["body"]["answer"] = "(rewritten)"
    assert verify(entries, anchors) == "entry 360: content doesn't match its hash"   # left as it is: caught
    assert verify(rehash(entries, 360), anchors) is None   # entries 350-375: not anchored yet


def test_cutting_the_end_is_caught_only_when_it_removes_an_anchored_entry():
    entries, anchors = build()
    assert verify(entries[:350]) is None and verify(entries[:350], anchors) is None
    assert verify(entries[:349]) is None
    assert verify(entries[:349], anchors) == "anchor at 349: the log ends at 348 (entries were cut off)"


def test_an_anchor_without_the_key_is_caught():
    entries, anchors = build()
    covered = rehash(entries[:122] + entries[123:], 122)
    forged = Anchors(every=50, key=anchors.key)
    forged.marks = [dict(m) for m in anchors.marks]
    for m in forged.marks:                            # the editor rewrites the anchors too, but can't sign
        if m["seq"] < len(covered):
            m["hash"] = covered[m["seq"]]["hash"]
    assert verify(covered, forged) == "anchor at 149: bad signature"


def test_the_labs_tamper_test():
    entries, anchors = build()
    assert tamper_test(entries, anchors) == {
        "edit an answer": (0, 200, 200),
        "swap two entries": (200, 200, 200),
        "delete an entry": (200, 200, 200),
        "delete, then renumber + rehash": (0, 0, 190),
        "edit, then rehash": (0, 0, 189),
        "insert a fake entry, rehash": (0, 0, 186),
        "cut off the end": (0, 0, 33),
    }


def test_the_trial_that_changed_nothing():
    # The video's run renamed the asker to "olivia": when the entry was already hers, nothing changed,
    # and the trial counted as a miss. 19 of the 200 did, so the anchors scored 173, not 189.
    entries, anchors = build()
    assert tamper_test(entries, anchors, new_user="olivia", kinds=["edit, then rehash"]) == {
        "edit, then rehash": (0, 0, 173)}
