"""Deterministic shared-history component tests; no chain or live identity tests."""
from copy import deepcopy
import hashlib
import json
from pathlib import Path
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import reference as s
c = s.c
NOW = 1800000000
SCOPE = {'issuer': 'https://attester.example/issuer',
         'sender_id': 'https://attester.example/senders/company', 'sender_epoch': 1}
TYPE = 'https://types.example/promotional'
KEY = Ed25519PrivateKey.from_private_bytes(hashlib.sha256(b'SR01 PUBLIC TEST KEY').digest())
PUBLIC = c.b64(KEY.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw))
POLICY = {'issuer': SCOPE['issuer'], 'sr01_admitted': True,
    'not_before': NOW-10000, 'expires_at': NOW+10000, 'clock_uncertainty': 0,
    'keys': {'TEST-ONLY': {'public_key': PUBLIC, 'roles': ['revocation_log', 'revocation_checkpoint', 'consent_revocation'],
        'not_before': NOW-10000, 'not_after': NOW+10000}},
    'revocation_methods': {'recipient': ['recipient-session-v1']}}

def signed(obj):
    return s.sign(obj, KEY, 'TEST-ONLY')

def fixture(count=2):
    previous = s.genesis(SCOPE)
    pairs = []
    for i in range(count):
        grant = c.b64(hashlib.sha256(('grant-'+str(i)).encode()).digest())
        event = {'profile': c.PROFILE, 'kind': 'revocation', 'issuer': SCOPE['issuer'],
            'event_id': f'urn:uuid:{i+1:08d}-1111-4111-8111-111111111111',
            'consent_id': f'urn:uuid:{i+100:08d}-2222-4222-8222-222222222222',
            'consent_commitment': grant, 'sequence': 1, 'previous_event_commitment': grant,
            'type_ids': [TYPE], 'actor': 'recipient', 'method': 'recipient-session-v1',
            'evidence_commitment': grant, 'requested_at': NOW-100, 'effective_at': NOW-90, 'issued_at': NOW-80}
        entry = {'profile': s.PROFILE, 'kind': 'revocation_entry', **SCOPE,
            'sequence': i+1, 'previous_entry_commitment': previous, 'event_commitment': c.commitment(event)}
        pairs.append({'entry': entry, 'revocation': c.sign(event, KEY, 'TEST-ONLY')})
        previous = s.hs(entry)
    head = {'profile': s.PROFILE, 'kind': 'revocation_log_head', **SCOPE,
        'sequence': count, 'event_head': previous, 'issued_at': NOW-60}
    cp = {'profile': s.PROFILE, 'kind': 'revocation_checkpoint', **SCOPE,
        'log_head_commitment': s.hs(head), 'covered_at': NOW-20, 'this_update': NOW-10, 'next_update': NOW+200}
    return pairs, head, cp

def run(pairs, head, cp, policy=None, replica=None, grant_time=NOW-1000, now=NOW):
    policy = deepcopy(policy or POLICY)
    replica = replica or s.Replica(SCOPE)
    replica.apply_page(pairs, policy, now)
    return replica.check_sync(signed(head), signed(cp), policy, now, grant_time)

results = []
def case(name, fn, expected=None):
    try:
        result = fn()
        assert expected is None, f'expected {expected}'
        if isinstance(result, dict) and 'can_assert_xcim_pass' in result:
            assert result['can_assert_xcim_pass'] is False and result['consent_result'] is None
        results.append({'name': name, 'passed': True, 'result': 'component-only'})
    except c.C01Error as e:
        results.append({'name': name, 'passed': e.code == expected, 'reason': e.code, 'expected': expected})
    except Exception as e:
        results.append({'name': name, 'passed': False, 'error': str(e)})

case('complete-bootstrap', lambda: run(*fixture()))
case('zero-revocations-with-authenticated-genesis', lambda: run(*fixture(0)))
case('duplicate-delivery', lambda: run(fixture()[0]*2, *fixture()[1:]))
case('missing-first-event', lambda: run(fixture()[0][1:], *fixture()[1:]), 'SR_GAP')
case('missing-last-event', lambda: run(fixture()[0][:1], *fixture()[1:]), 'SR_INCOMPLETE_OR_ROLLBACK')
case('reordered-events', lambda: run(list(reversed(fixture()[0])), *fixture()[1:]), 'SR_GAP')
case('empty-response-is-not-complete', lambda: run([], *fixture()[1:]), 'SR_INCOMPLETE_OR_ROLLBACK')

def mutate(part, field, value, expected, name):
    def task():
        pairs, head, cp = fixture()
        target = {'head': head, 'checkpoint': cp, 'entry': pairs[0]['entry']}[part]
        target[field] = value
        if part == 'head':
            cp['log_head_commitment'] = s.hs(head)
        return run(pairs, head, cp)
    case(name, task, expected)

mutate('entry', 'sender_epoch', 2, 'SR_SCOPE', 'cross-epoch-entry')
mutate('head', 'sender_id', 'https://other.example/sender', 'SR_SCOPE', 'cross-sender-head')
mutate('checkpoint', 'sender_epoch', 2, 'SR_SCOPE', 'cross-epoch-checkpoint')
mutate('checkpoint', 'profile', c.PROFILE, 'SR_SCHEMA', 'no-profile-relabel')
mutate('checkpoint', 'extra', True, 'SR_SCHEMA', 'closed-checkpoint')
mutate('checkpoint', 'next_update', NOW+301, 'SR_INTERVAL', 'oversized-window')
mutate('checkpoint', 'next_update', NOW, 'SR_STALE', 'deadline-equality')
mutate('checkpoint', 'this_update', NOW+1, 'SR_STALE', 'future-statement')
mutate('checkpoint', 'covered_at', NOW-301, 'SR_INTERVAL', 'new-signature-old-coverage')
mutate('head', 'issued_at', NOW-200, 'SR_HEAD_TIME', 'head-before-last-event')
mutate('entry', 'event_commitment', c.b64(bytes(32)), 'SR_EVENT_COMMITMENT', 'substituted-event')
mutate('entry', 'previous_entry_commitment', c.b64(bytes(32)), 'SR_GAP', 'broken-prefix')
case('grant-newer-than-cut', lambda: run(*fixture(), grant_time=NOW-5), 'SR_GRANT_AFTER_CUT')
case('cached-checkpoint-does-not-restart-time', lambda: run(*fixture(), now=NOW+201), 'SR_STALE')

def policy_case(field, value, expected, name):
    p = deepcopy(POLICY)
    p[field] = value
    case(name, lambda: run(*fixture(), policy=p), expected)
policy_case('sr01_admitted', False, 'SR_NOT_ADMITTED', 'explicit-profile-admission')
policy_case('clock_uncertainty', 61, 'SR_CLOCK', 'unbounded-clock')
policy_case('expires_at', NOW, 'SR_POLICY_TIME', 'expired-policy')
policy_case('clock_uncertainty', 11, 'SR_STALE', 'conservative-clock-bound')

def key_case(name, field, value, expected):
    p = deepcopy(POLICY)
    p['keys']['TEST-ONLY'][field] = value
    case(name, lambda: run(*fixture(), policy=p), expected)
key_case('wrong-checkpoint-role', 'roles', ['consent_revocation', 'revocation_log'], 'SR_KEY_ROLE')
key_case('expired-current-checkpoint-key', 'not_after', NOW, 'SR_KEY_CURRENT')

def bad_sig():
    pairs, head, cp = fixture()
    pairs[0]['revocation']['signature'] = c.b64(bytes(64))
    return run(pairs, head, cp)
case('forged-withdrawal', bad_sig, 'SIGNATURE')

def partial_incremental():
    pairs, head, cp = fixture()
    replica = s.Replica(SCOPE)
    replica.learn_head(signed(head), POLICY)
    replica.apply_page(pairs[:1], POLICY, NOW)
    try:
        replica.check_sync(signed(head), signed(cp), POLICY, NOW, NOW-1000)
        raise AssertionError('partial history accepted')
    except c.C01Error as e:
        assert e.code == 'SR_INCOMPLETE_OR_ROLLBACK'
    replica.apply_page(pairs[1:], POLICY, NOW)
    return replica.check_sync(signed(head), signed(cp), POLICY, NOW, NOW-1000)
case('incremental-catch-up', partial_incremental)

def rollback():
    pairs, head, cp = fixture()
    replica = s.Replica(SCOPE)
    run(pairs, head, cp, replica=replica)
    return run(*fixture(0), replica=replica)
case('known-newer-head-blocks-old-view', rollback, 'SR_INCOMPLETE_OR_ROLLBACK')

def coverage_rollback():
    pairs, head, cp = fixture()
    replica = s.Replica(SCOPE)
    run(pairs, head, cp, replica=replica)
    cp['covered_at'] -= 1
    return run([], head, cp, replica=replica)
case('coverage-floor', coverage_rollback, 'SR_COVERAGE_ROLLBACK')

def conflict():
    _, head, _ = fixture()
    replica = s.Replica(SCOPE)
    replica.learn_head(signed(head), POLICY)
    head['issued_at'] += 1
    return replica.learn_head(signed(head), POLICY)
case('same-sequence-conflict', conflict, 'SR_HEAD_CONFLICT')

def atomic_failure():
    pairs, _, _ = fixture()
    replica = s.Replica(SCOPE)
    pairs[1]['entry']['sequence'] += 1
    try:
        replica.apply_page(pairs, POLICY, NOW)
    except c.C01Error:
        assert replica.sequence == 0 and replica.events == {}
        return
    raise AssertionError('bad page accepted')
case('failed-page-does-not-partially-advance', atomic_failure)

def signature_check():
    _, head, cp = fixture(0)
    env = signed(cp)
    env['signature'] = c.b64(bytes(64))
    return s.Replica(SCOPE).check_sync(signed(head), env, POLICY, NOW, NOW-1000)
case('forged-checkpoint', signature_check, 'SR_SIGNATURE')

root = Path(__file__).resolve().parent
report = {'profile': s.PROFILE, 'scope': 'shared-history-component-only',
    'test_only': True, 'can_assert_xcim_pass': False, 'integrated_consent_verifier': False,
    'durable_storage_tested': False, 'live_blockchain_tested': False,
    'total': len(results), 'passed': sum(r['passed'] for r in results),
    'failed': sum(not r['passed'] for r in results), 'cases': results}
(root/'test-report.json').write_text(json.dumps(report, indent=2)+'\n', encoding='utf-8')
pairs, head, cp = fixture()
(root/'example.json').write_text(json.dumps({'test_only': True, 'scope': SCOPE,
    'policy': POLICY, 'evaluation_time': NOW, 'pairs': pairs,
    'log_head': signed(head), 'checkpoint': signed(cp)}, indent=2)+'\n', encoding='utf-8')
print(json.dumps({k: report[k] for k in ('total','passed','failed','scope')}))
assert report['failed'] == 0, [r for r in results if not r['passed']]
