"""Seven sequential retry cases using synthetic faults and tiny JSON state files.

Standard library only. No HTTP, API, email, payments, real network timeout,
sleep, or existing application data. Each invocation creates a new directory.
The single-process fixture is educational, not a production idempotency store.
"""
import argparse
import datetime as dt
import hashlib
import json
from pathlib import Path
import platform
import tempfile


class SyntheticTimeout(Exception):
    pass


class KeyConflict(Exception):
    pass


def run(out):
    out.mkdir(parents=True, exist_ok=True)
    root = Path(tempfile.mkdtemp(prefix='retry-effects-', dir=out))
    cases = []

    def case(name, keys, faults, expected_effects, expected_outcome, payloads=None):
        state_file = root / (name + '.json')
        state_file.write_text(json.dumps({'effects': [], 'keys': {}}), encoding='utf-8')
        events = []
        attempts = []

        def call(key, payload, fault, attempt):
            state = json.loads(state_file.read_text(encoding='utf-8'))
            digest = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
            if fault == 'before_effect':
                events.append({'attempt': attempt, 'event': 'injected_timeout_before_effect'})
                raise SyntheticTimeout()
            if key is not None and key in state['keys']:
                saved = state['keys'][key]
                if saved['payload_sha256'] != digest:
                    events.append({'attempt': attempt, 'event': 'key_payload_conflict'})
                    raise KeyConflict()
                response = saved['response']
                events.append({'attempt': attempt, 'event': 'replayed', 'effect_id': response['effect_id']})
            else:
                response = {'effect_id': len(state['effects']) + 1}
                state['effects'].append({'effect_id': response['effect_id'], 'payload': payload})
                if key is not None:
                    state['keys'][key] = {'payload_sha256': digest, 'response': response}
                # One state snapshot contains the effect AND its key record.
                # One caller only; no crash/power-loss or concurrent-worker claims.
                temp = state_file.with_suffix('.pending')
                temp.write_text(json.dumps(state), encoding='utf-8')
                temp.replace(state_file)
                events.append({'attempt': attempt, 'event': 'committed', 'effect_id': response['effect_id']})
            if fault == 'after_commit':
                events.append({'attempt': attempt, 'event': 'injected_timeout_after_commit'})
                raise SyntheticTimeout()
            return response

        outcome = 'attempt_limit'
        for index, (key, fault) in enumerate(zip(keys, faults), 1):
            payload = payloads[index - 1] if payloads else {'job': 'synthetic-report', 'period': '2026-11-06'}
            try:
                response = call(key, payload, fault, index)
                attempts.append({'attempt': index, 'key': key, 'status': 'success', 'response': response})
                outcome = 'success'
                break
            except SyntheticTimeout:
                attempts.append({'attempt': index, 'key': key, 'status': 'timeout_outcome_unknown'})
            except KeyConflict:
                attempts.append({'attempt': index, 'key': key, 'status': 'conflict_stop'})
                outcome = 'conflict'
                break
        state = json.loads(state_file.read_text(encoding='utf-8'))
        checks = {
            'expected_effect_count': len(state['effects']) == expected_effects,
            'expected_outcome': outcome == expected_outcome,
            'attempts_bounded': len(attempts) <= len(keys),
            'effects_match_commit_events': len(state['effects']) == sum(e['event'] == 'committed' for e in events),
            'key_records_reference_effects': all(r['response']['effect_id'] in [e['effect_id'] for e in state['effects']] for r in state['keys'].values()),
        }
        cases.append(dict(name=name, max_attempts=len(keys), attempts=attempts, events=events,
                          final_state=state, effect_count=len(state['effects']), outcome=outcome,
                          expected_effects=expected_effects, expected_outcome=expected_outcome,
                          checks=checks, passed=all(checks.values())))

    case('before_effect_retry', [None]*2, ['before_effect', None], 1, 'success')
    case('lost_response_naive_retry', [None]*2, ['after_commit', None], 2, 'success')
    case('lost_response_same_key', ['run-001']*2, ['after_commit', None], 1, 'success')
    case('lost_response_changed_key', ['run-001', 'run-002'], ['after_commit', None], 2, 'success')
    case('same_key_changed_payload', ['run-001']*2, ['after_commit', None], 1, 'conflict',
         [{'period': '2026-11-06'}, {'period': '2026-11-07'}])
    case('transient_then_success', ['run-001']*3, ['before_effect', 'before_effect', None], 1, 'success')
    case('retry_budget_exhausted', ['run-001']*3, ['before_effect']*3, 0, 'attempt_limit')
    result = dict(completed_at_utc=dt.datetime.now(dt.timezone.utc).isoformat(),
                  environment={'os': platform.system(), 'kernel': platform.release(), 'python': platform.python_version()},
                  cases=cases, passed=all(c['passed'] for c in cases),
                  scope='Seven sequential fault-injection cases in newly created JSON fixtures. Timeout exceptions are injected before an effect or after a committed snapshot; no HTTP, real network timeout, provider calls, timing/backoff benchmark, concurrency, restart, crash durability, key expiry or distributed exactly-once guarantee is tested.')
    (root / 'result.json').write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8')
    print(json.dumps({'result_file': str(root / 'result.json'), 'passed': result['passed']}))
    return 0 if result['passed'] else 1


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--out', type=Path, required=True)
    raise SystemExit(run(parser.parse_args().out))
