"""Bounded direct Agrippa decoder test, with independent poem recovery control.

Algorithm source: Ayal Ryger's implementation, archived by UCSB:
https://agrippa.english.ucsb.edu/wp-content/uploads/images/cracked-code/submissions/files/agrippa_in_action.html
This is a fresh arithmetic implementation; not the disk's self-destruct code.
Source poem bytes are local validation inputs, not redistributed in our bundle.
"""
from pathlib import Path
import hashlib
import json

ROOT = Path(__file__).resolve().parent


def permute(x, start, step):
    return sum(((x >> i) & 1) << ((start + step * i) % 8) for i in range(8))


def decode(data):
    if len(data) % 3:
        raise ValueError('Complete three-byte blocks required; no implicit padding')
    out = []
    overflows = []
    for offset in range(0, len(data), 3):
        a, b, c = data[offset:offset+3]
        middle = permute(b, 1, 3)
        lo = pow(a | ((middle & 15) << 8), 11, 4097)
        hi = pow((middle >> 4) | (c << 4), 11, 4097)
        # A residue of 4096 exceeds the nominal 12-bit container.
        # Record rather than silently treating this as a clean reversible block.
        if max(lo, hi) > 4095:
            overflows.append(offset)
        packed = lo | (hi << 12)
        out.extend(permute((packed >> shift) & 255, 2, 5) for shift in (0, 8, 16))
    return bytes(out), overflows


def digest(data):
    return hashlib.sha256(data).hexdigest()


def readability(data):
    allowed = set(range(32, 127)) | {9, 10, 13}
    longest = run = 0
    for x in data:
        run = run + 1 if x in allowed else 0
        longest = max(longest, run)
    return {'ascii_text_bytes': sum(x in allowed for x in data),
            'total_bytes': len(data), 'longest_ascii_text_run': longest,
            'ascii_text_fraction': sum(x in allowed for x in data) / len(data)}


def main():
    ciphertext = (ROOT/'agrippa_cipher.bin').read_bytes()
    expected = (ROOT/'agrippa_memory_plain.bin').read_bytes()
    js = (ROOT/'agrippa_js_plain.bin').read_bytes()
    actual, overflow = decode(ciphertext)
    assert actual == js, 'Published implementation comparison failed'
    # The independent 2011 disk-memory extraction uses Mac CR newlines;
    # the demonstration ciphertext decodes to LF. No other normalization.
    assert actual == expected.replace(b'\r', b'\n'), 'Independent recovery control failed'
    assert not overflow
    blob = (ROOT/'base60_verified.bin').read_bytes()
    assert len(blob) == 256
    assert digest(blob) == '3b9b07d9a26e6d55c432d94d2661fdff3c2b348daed06821f2bdb23184a4b290'
    # Declared scope: original byte order, published constants, all three phases.
    # Ignore only incomplete edge blocks, record exact omissions, never pad.
    trials = []
    for phase in range(3):
        end = phase + ((len(blob)-phase)//3)*3
        plain, overflow = decode(blob[phase:end])
        trials.append({'start_offset': phase, 'end_exclusive': end,
                       'omitted_prefix_hex': blob[:phase].hex(),
                       'omitted_suffix_hex': blob[end:].hex(),
                       'overflow_block_offsets_relative_to_slice': overflow,
                       'output_hex': plain.hex(), 'readability': readability(plain)})
    result = {'method': 'Published Agrippa decryption, exponent 11, modulus 4097, fixed bit permutations',
              'control': {'bytes': len(actual), 'matches_independent_memory_dump_after_CR_to_LF': True,
                          'memory_dump_raw_sha256': digest(expected),
                          'line_endings_normalized': expected.count(b'\r'),
                          'matches_published_javascript': True,
                          'plaintext_sha256': digest(actual), 'ciphertext_sha256': digest(ciphertext),
                          'overflow_blocks': 0},
              'target': {'bytes': len(blob), 'sha256': digest(blob),
                         'whole_block_divisible_by_three': len(blob) % 3 == 0},
              'trials': trials,
              'limits': ['Only direct published algorithm, original byte order, and three byte alignments.',
                         'Edge omissions do not establish a header or trailer.',
                         'ASCII readability does not test compressed data, another encoding, or another encryption layer.',
                         'Rune-to-byte recodings and modified algorithms not tested.']}
    (ROOT/'agrippa_results.json').write_text(json.dumps(result, indent=2)+'\n')
    print(json.dumps({**result, 'trials': [{k:v for k,v in t.items() if k != 'output_hex'} for t in trials]}, indent=2))


if __name__ == '__main__':
    main()
