"""Pins every claim the commentary makes, so the page can't rot.

Stdlib asserts, no pytest. Runs in a few seconds; the two expensive tests mine
the same 8-block 18-bit chain the demo's act 3 uses.
"""

from chain import (Block, GENESIS_PREV, build_chain, choose_heaviest,
                   choose_longest, counted, counter_reset, meets, mine,
                   tamper_and_remine, total_work, validate, work)

DATAS = [f"tx{i}: alice pays bob {i}".encode() for i in range(8)]
MALLORY = b"tx3: alice pays MALLORY 1000000"


def attack_chain(honest, fork_at, bits):
    """The act-3 attack: fork, rewrite, and mine ONE block more than honest."""
    before = counted()
    out = list(honest[:fork_at])
    prev = honest[fork_at - 1].hash()
    for i in range(fork_at, len(honest) + 1):
        d = MALLORY if i == fork_at else f"tx{i}: filler".encode()
        b, _ = mine(i, prev, d, bits)
        out.append(b)
        prev = b.hash()
    return out, counted() - before


def test_honest_chain_validates_and_every_block_meets_its_target():
    chain, _ = build_chain(DATAS, 12)
    assert validate(chain) == (True, "valid")
    assert chain[0].prev == GENESIS_PREV
    for i, b in enumerate(chain):
        assert b.index == i
        assert meets(b.hash(), b.bits), i
        assert b.prev == (chain[i - 1].hash() if i else GENESIS_PREV)


def test_mining_is_deterministic():
    """Nonce search starts at 0, so the same header costs the same hashes."""
    a, ca = mine(0, GENESIS_PREV, b"same", 12)
    b, cb = mine(0, GENESIS_PREV, b"same", 12)
    assert (a.nonce, ca) == (b.nonce, cb)
    assert a.hash() == b.hash()


def test_tampering_breaks_exactly_one_link_and_one_proof_of_work():
    """The recorded intuition says 'every block after it is invalidated'.

    Measured: one link (block k+1's) and one PoW (block k's), at every n and
    k tried. Blocks k+2..n-1 are individually pristine.
    """
    for n in (6, 8, 12, 16):
        chain, _ = build_chain([f"tx{i}".encode() for i in range(n)], 10)
        for k in (1, n // 2, n - 2):
            t = [Block(b.index, b.prev, b.data, b.bits, b.nonce) for b in chain]
            t[k].data += b"!"
            links = sum(b.prev != (t[i - 1].hash() if i else GENESIS_PREV)
                        for i, b in enumerate(t))
            pows = sum(not meets(b.hash(), b.bits) for b in t)
            intact = sum(1 for i in range(k + 1, n)
                         if t[i].prev == t[i - 1].hash()
                         and meets(t[i].hash(), t[i].bits))
            assert (links, pows) == (1, 1), (n, k, links, pows)
            assert intact == n - k - 2, (n, k, intact)


def test_tamper_and_remine_is_indistinguishable_from_honest():
    chain, _ = build_chain(DATAS, 12)
    forged, cost = tamper_and_remine(chain, 3, MALLORY)
    assert validate(forged) == (True, "valid")
    assert len(forged) == len(chain)
    assert total_work(forged) == total_work(chain)
    assert forged[3].data == MALLORY
    assert forged[-1].hash() != chain[-1].hash()
    assert cost > 0


def test_the_attack_costs_21_hashes_against_2638572():
    """The headline. Both numbers are exact: the nonce search is a pure
    function of the header, so they do not move between runs."""
    counter_reset()
    honest, hon_cost = build_chain(DATAS, 18)
    attack, att_cost = attack_chain(honest, 3, 1)
    assert (hon_cost, att_cost) == (2638572, 21), (hon_cost, att_cost)
    assert (len(honest), len(attack)) == (8, 9)
    assert validate(attack) == (True, "valid")


def test_the_two_by_two_has_exactly_one_broken_cell():
    counter_reset()
    honest, _ = build_chain(DATAS, 18)
    attack, _ = attack_chain(honest, 3, 1)
    rewritten = {}
    for pin in (False, True):
        for bywork in (False, True):
            ok, _ = validate(attack, expected_bits=18 if pin else None)
            chooser = choose_heaviest if bywork else choose_longest
            rewritten[pin, bywork] = ok and chooser(honest, attack) is attack
    assert rewritten == {(False, False): True, (False, True): False,
                         (True, False): False, (True, True): False}


def test_total_work_arithmetic():
    """3 inherited blocks at 18 bits + 6 new ones at 1 bit = 786,444."""
    counter_reset()
    honest, _ = build_chain(DATAS, 18)
    attack, _ = attack_chain(honest, 3, 1)
    assert total_work(honest) == 8 * work(18) == 2097152
    assert total_work(attack) == 3 * work(18) + 6 * work(1) == 786444
    assert len(attack) > len(honest)
    assert total_work(attack) < total_work(honest)


def test_the_boundary_is_the_honest_difficulty():
    """Below 18 declared bits the work rule holds; at 18 it hands over."""
    honest_work = 8 * work(18)
    for ab in range(1, 21):
        att_work = 3 * work(18) + 6 * work(ab)
        assert (att_work >= honest_work) == (ab >= 18), ab
    assert 3 * work(18) + 6 * work(17) == 1572864
    assert 3 * work(18) + 6 * work(18) == 2359296


def test_validate_catches_what_it_is_meant_to_catch():
    chain, _ = build_chain(DATAS, 10)
    assert validate([]) == (False, "empty")
    bad_genesis = [Block(b.index, b.prev, b.data, b.bits, b.nonce) for b in chain]
    bad_genesis[0].prev = b"\x01" * 32
    assert validate(bad_genesis)[0] is False
    relabelled = [Block(b.index, b.prev, b.data, b.bits, b.nonce) for b in chain]
    relabelled[2].bits = 1            # claim it was easy, remine nothing
    ok, why = validate(relabelled)
    assert ok is False and "hash pointer" in why   # bits is inside the header
    assert validate(chain, expected_bits=10)[0] is True
    assert validate(chain, expected_bits=11)[0] is False


TESTS = [
    test_honest_chain_validates_and_every_block_meets_its_target,
    test_mining_is_deterministic,
    test_tampering_breaks_exactly_one_link_and_one_proof_of_work,
    test_tamper_and_remine_is_indistinguishable_from_honest,
    test_the_attack_costs_21_hashes_against_2638572,
    test_the_two_by_two_has_exactly_one_broken_cell,
    test_total_work_arithmetic,
    test_the_boundary_is_the_honest_difficulty,
    test_validate_catches_what_it_is_meant_to_catch,
]


if __name__ == "__main__":
    failed = 0
    for test in TESTS:
        try:
            test()
        except AssertionError as exc:
            failed += 1
            print(f"FAIL  {test.__name__}: {exc}")
        else:
            print(f"PASS  {test.__name__}")
    if failed:
        print(f"\n{failed} of {len(TESTS)} tests FAILED")
        raise SystemExit(1)
    print(f"\nAll {len(TESTS)} tests PASSED")
