"""Exercice fictif, sans réseau ni clé. Ne décide aucune opération financière.

Les entrées sont des observations locales simplifiées, pas des réponses API.
La limite de fraîcheur de 15 minutes est une politique de recette, pas une règle Stripe.
Exécuter : python3 diagnostic_connect.py
"""
from dataclasses import dataclass, replace
import json


@dataclass(frozen=True)
class Observation:
    charges: bool
    payouts: bool
    age_minutes: int = 0
    currently_due: bool = False
    pending_review: bool = False
    dedicated_channel: bool = False  # Canal déterminé après diagnostic humain.
    returned_from_onboarding: bool = False


def diagnostic(o, fault=None):
    if o.age_minutes < 0:
        raise ValueError('Une observation ne peut pas venir du futur.')
    if o.age_minutes > 15 and fault != 'ignore_freshness':
        return {'functions': 'unknown', 'actions': ['refresh']}
    charges, payouts = o.charges, o.payouts
    if fault == 'return_means_enabled' and o.returned_from_onboarding:
        charges = payouts = True
    actions = []
    if o.currently_due:
        actions.append('dedicated_channel' if o.dedicated_channel else 'seller_action')
    if o.pending_review:
        actions.append('follow_review')
    if o.dedicated_channel and 'dedicated_channel' not in actions:
        actions.append('dedicated_channel')
    functions = 'observed_available' if charges and payouts else 'restricted'
    if not actions and functions == 'restricted':
        actions.append('diagnose_reason')
    if fault == 'enabled_means_nothing_due' and functions == 'observed_available':
        actions = []
    return {'functions': functions, 'actions': actions}


def allocation(fault=None):
    # Montants en centimes, commission fictive 10 %, sans frais ni fiscalité.
    gross = {'A': 60000, 'B': 40000}
    commissions = {seller: amount // 10 for seller, amount in gross.items()}
    entitlements = {seller: gross[seller] - commissions[seller] for seller in gross}
    transferred = {'A': 54000, 'B': 0}
    # Aucun virement n'est créé dans l'exercice, même pour le transfert établi d'A.
    bank_payout = {'A': 0, 'B': 0}
    if fault == 'transfer_means_payout':
        bank_payout = transferred.copy()
    return {
        'commissions': commissions,
        'entitlements': entitlements,
        'to_transfer': sum(entitlements[s] - transferred[s] for s in gross),
        'transferred_without_payout': sum(transferred[s] - bank_payout[s] for s in gross),
        'bank_payout': bank_payout,
    }


def run_checks(fault=None):
    passed = []
    def check(name, condition):
        assert condition, name
        passed.append(name)
    base = Observation(True, True)
    check('available_observation', diagnostic(base, fault) == {'functions': 'observed_available', 'actions': []})
    check('still_due_when_available', diagnostic(replace(base, currently_due=True), fault)['actions'] == ['seller_action'])
    check('review_and_correction_coexist', diagnostic(replace(base, currently_due=True, pending_review=True), fault)['actions'] == ['seller_action', 'follow_review'])
    check('payout_restricted', diagnostic(replace(base, payouts=False), fault)['functions'] == 'restricted')
    check('unknown_reason_not_identity_request', diagnostic(replace(base, payouts=False), fault)['actions'] == ['diagnose_reason'])
    check('review_only', diagnostic(replace(base, payouts=False, pending_review=True), fault)['actions'] == ['follow_review'])
    check('dedicated_channel', diagnostic(replace(base, currently_due=True, dedicated_channel=True), fault)['actions'] == ['dedicated_channel'])
    check('freshness_boundary_15', diagnostic(replace(base, age_minutes=15), fault)['functions'] == 'observed_available')
    check('stale_16', diagnostic(replace(base, age_minutes=16), fault) == {'functions': 'unknown', 'actions': ['refresh']})
    check('return_is_not_enablement', diagnostic(replace(base, charges=False, payouts=False, returned_from_onboarding=True), fault)['functions'] == 'restricted')
    check('return_does_not_clear_due', diagnostic(replace(base, currently_due=True, returned_from_onboarding=True), fault)['actions'] == ['seller_action'])
    try:
        diagnostic(replace(base, age_minutes=-1), fault)
    except ValueError:
        passed.append('future_observation_rejected')
    else:
        raise AssertionError('future_observation_rejected')
    amounts = allocation(fault)
    check('commission_100_euros', sum(amounts['commissions'].values()) == 10000)
    check('seller_a_540_euros', amounts['entitlements']['A'] == 54000)
    check('seller_b_360_euros', amounts['entitlements']['B'] == 36000)
    check('remaining_transfer_360_euros', amounts['to_transfer'] == 36000)
    check('transferred_not_paid_out_540_euros', amounts['transferred_without_payout'] == 54000)
    check('exclusive_exposure_900_euros', amounts['to_transfer'] + amounts['transferred_without_payout'] == 90000)
    check('no_bank_payout_created', sum(amounts['bank_payout'].values()) == 0)
    return passed


if __name__ == '__main__':
    checks = run_checks()
    detected = {}
    for mutation in ('ignore_freshness', 'return_means_enabled',
                     'enabled_means_nothing_due', 'transfer_means_payout'):
        try:
            run_checks(mutation)
        except AssertionError as error:
            detected[mutation] = str(error)
        else:
            raise AssertionError('Mutation non détectée : ' + mutation)
    print(json.dumps({'checks': len(checks), 'passed': checks,
                      'mutations_detected': detected, 'allocation_cents': allocation(),
                      'scope': 'Fiction locale, aucun appel Stripe ni mouvement financier.'},
                     ensure_ascii=False, indent=2))
