"""Recette locale d'un import de résultats manuscrits, sans API ni modèle."""
from copy import deepcopy
from itertools import permutations

BATCH = 'lot-A'
MANIFEST = {
    'r01': {'dossier': 'D101', 'revision': 3, 'supplier_ref': 'DEV-11'},
    'r02': {'dossier': 'D102', 'revision': 1, 'supplier_ref': 'DEV-12'},
    'r03': {'dossier': 'D103', 'revision': 2, 'supplier_ref': 'DEV-13'},
    'r04': {'dossier': 'D104', 'revision': 4, 'supplier_ref': 'DEV-14'},
}
COUNTS = {'succeeded': 2, 'errored': 1, 'expired': 1, 'canceled': 0}
RESULTS = [
    {'custom_id': 'r03', 'result': {'type': 'succeeded', 'stop_reason': 'end_turn',
                                 'data': {'supplier_ref': 'DEV-13', 'amount_cents': 90000}}},
    {'custom_id': 'r01', 'result': {'type': 'succeeded', 'stop_reason': 'end_turn',
                                 'data': {'supplier_ref': 'DEV-11', 'amount_cents': 12000}}},
    {'custom_id': 'r04', 'result': {'type': 'expired'}},
    {'custom_id': 'r02', 'result': {'type': 'errored'}},
]


def reconcile(batch_id, processing_status, rows, counts, current):
    """Adaptateur simplifié : stop_reason/data sont extraits de message en amont.
    current vient du système métier, jamais du texte produit par le modèle.
    Tous les résultats sont reçus en staging avant une décision de promotion.
    """
    if batch_id != BATCH or processing_status != 'ended':
        raise ValueError('wrong_batch_or_not_ended')
    if set(counts) != {'succeeded', 'errored', 'expired', 'canceled'}:
        raise ValueError('count_shape')
    if any(type(v) is not int or v < 0 for v in counts.values()):
        raise ValueError('count_value')
    by_id = {}
    for row in rows:
        cid = row['custom_id']
        if cid not in MANIFEST or cid in by_id:
            raise ValueError('unknown_or_duplicate_id')
        if row['result']['type'] not in counts:
            raise ValueError('unknown_result_type')
        by_id[cid] = row['result']
    if set(by_id) != set(MANIFEST):
        raise ValueError('missing_id')
    actual = {state: sum(x['type'] == state for x in by_id.values()) for state in counts}
    if actual != counts:
        raise ValueError('counts_mismatch')
    output = {}
    for cid, context in MANIFEST.items():
        result = by_id[cid]
        dossier = context['dossier']
        if current.get(dossier) != context['revision']:
            output[dossier] = {'status': 'stale_revision'}
            continue
        if result['type'] != 'succeeded':
            output[dossier] = {'status': result['type']}
            continue
        if result.get('stop_reason') != 'end_turn':
            output[dossier] = {'status': 'incomplete'}
            continue
        data = result.get('data')
        if (not isinstance(data, dict) or set(data) != {'supplier_ref', 'amount_cents'}
            or data['supplier_ref'] != context['supplier_ref']
            or type(data['amount_cents']) is not int or data['amount_cents'] < 0):
            output[dossier] = {'status': 'invalid_data'}
            continue
        output[dossier] = {'status': 'candidate', 'amount_cents': data['amount_cents']}
    return output


def run():
    checks = 0
    def check(value):
        nonlocal checks
        assert value
        checks += 1
    current = {x['dossier']: x['revision'] for x in MANIFEST.values()}
    expected = {'D101': {'status': 'candidate', 'amount_cents': 12000},
                'D102': {'status': 'errored'},
                'D103': {'status': 'candidate', 'amount_cents': 90000},
                'D104': {'status': 'expired'}}
    def run_rows(rows, counts=COUNTS, state='ended', batch=BATCH, revisions=current):
        return reconcile(batch, state, rows, counts, revisions)
    def rejects(rows, **kwargs):
        try:
            run_rows(rows, **kwargs)
        except ValueError:
            return True
        return False
    for order in permutations(RESULTS):
        check(run_rows(order) == expected)
    # Témoin1 : zip sur les seules réussites inverse les dossiers malgré un total identique.
    wrong = dict(zip(['D101', 'D103'], [90000, 12000]))
    check(wrong != {'D101': 12000, 'D103': 90000})
    check(sum(wrong.values()) == 102000)  # total1020EUR : aucune preuve d'affectation
    check(rejects(RESULTS[:-1]))
    check(rejects(RESULTS + [deepcopy(RESULTS[0])]))
    unknown = deepcopy(RESULTS); unknown[0]['custom_id'] = 'other'
    check(rejects(unknown))
    check(rejects(RESULTS, batch='lot-B'))
    check(rejects(RESULTS, state='in_progress'))
    check(rejects(RESULTS, state='canceling'))
    check(rejects(RESULTS, counts=dict(COUNTS, succeeded=3)))
    check(rejects(RESULTS, counts=dict(COUNTS, succeeded=True)))
    # Témoin2 : ended n'implique pas quatre candidats ; seulement deux ici.
    check(sum(r['status'] == 'candidate' for r in run_rows(RESULTS).values()) == 2)
    canceled = deepcopy(RESULTS); canceled[2]['result']['type'] = 'canceled'
    check(run_rows(canceled, counts=dict(COUNTS, expired=0, canceled=1))['D104']['status'] == 'canceled')
    # Témoin3 : succeeded avec génération arrêtée trop tôt reste incomplet.
    cut = deepcopy(RESULTS); cut[0]['result']['stop_reason'] = 'max_tokens'
    check(run_rows(cut)['D103']['status'] == 'incomplete')
    check(run_rows(RESULTS, revisions=dict(current, D101=4))['D101']['status'] == 'stale_revision')
    for value in [True, -1, '12000', 120.0, None]:
        bad = deepcopy(RESULTS); bad[1]['result']['data']['amount_cents'] = value
        check(run_rows(bad)['D101']['status'] == 'invalid_data')
    bad = deepcopy(RESULTS); bad[1]['result']['data']['supplier_ref'] = 'DEV-13'
    check(run_rows(bad)['D101']['status'] == 'invalid_data')
    bad = deepcopy(RESULTS); bad[0]['result']['type'] = 'unknown'
    check(rejects(bad))
    print(f'{checks} contrôles réussis ; 24 permutations ; 3 témoins insuffisants ; aucun modèle ni ERP appelé.')

if __name__ == '__main__':
    run()
