"""Exercice fictif local, sans API Amazon ni connexion bancaire.

Montants en centimes. Les révisions et références de banque sont des données
normalisées de l'exercice, pas des champs ni des garanties de la SP-API.
"""
from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Observation:
    seller: str
    transaction: str
    revision: int
    currency: str
    cents: int
    status: str
    group: str | None = None


def latest(rows):
    result = {}
    for row in rows:
        key = (row.seller, row.transaction)
        old = result.get(key)
        if old and old.revision == row.revision and old != row:
            raise ValueError("Observations contradictoires à départager")
        if old is None or row.revision > old.revision:
            result[key] = row
    return list(result.values())


def buckets(rows, seller, currency):
    amounts = {"deferred": 0, "released_unassigned": 0, "grouped": 0}
    for row in latest(rows):
        if (row.seller, row.currency) != (seller, currency):
            continue
        if row.status == "DEFERRED":
            if row.group:
                raise ValueError("Différé et groupe : vérifier la projection")
            amounts["deferred"] += row.cents
        elif row.status in ("RELEASED", "DEFERRED_RELEASED"):
            amounts["grouped" if row.group else "released_unassigned"] += row.cents
        else:
            raise ValueError("Statut non qualifié")
    return amounts


def reconcile(rows, *, seller="S1", currency="EUR", group="G1",
              complete=True, closed=True, original=18000,
              bank_seller="S1", bank_currency="EUR", bank_group="G1",
              bank_cents=17500):
    if not complete or not closed:
        return "WAIT", None
    # Application volontairement bornée au filtre RELEASED du tutoriel Amazon.
    members = [r for r in latest(rows) if
               (r.seller, r.currency, r.group) == (seller, currency, group)]
    if not members or any(r.status != "RELEASED" for r in members):
        return "REVIEW", None
    if sum(r.cents for r in members) != original:
        return "GROUP_GAP", None
    if (bank_seller, bank_currency, bank_group) != (seller, currency, group):
        return "UNMATCHED", None
    if bank_cents is None:
        return "NO_RECEIPT", None
    gap = original - bank_cents
    return ("MATCHED" if gap == 0 else "BANK_GAP"), gap


T1 = Observation("S1", "T1", 1, "EUR", 12000, "DEFERRED")
rows = [T1, replace(T1, revision=2, status="DEFERRED_RELEASED"),
        Observation("S1", "T2", 1, "EUR", 20000, "RELEASED", "G1"),
        Observation("S1", "T3", 1, "EUR", -2000, "RELEASED", "G1"),
        Observation("S1", "T4", 1, "EUR", 20000, "DEFERRED"),
        Observation("S1", "T5", 1, "USD", 5000, "RELEASED")]


def run():
    checks = 0

    def check(condition):
        nonlocal checks
        assert condition
        checks += 1

    def rejects(call):
        try:
            call()
        except ValueError:
            return True
        return False

    expected = {"deferred": 20000, "released_unassigned": 12000, "grouped": 18000}
    check(buckets(rows, "S1", "EUR") == expected)
    check(buckets(rows * 2, "S1", "EUR") == expected)
    check(buckets(list(reversed(rows)), "S1", "EUR") == expected)
    check(sum(expected.values()) == 50000)
    check(buckets(rows, "S1", "USD")["released_unassigned"] == 5000)
    check(reconcile(rows) == ("BANK_GAP", 500))
    check(reconcile(rows, bank_cents=18000) == ("MATCHED", 0))
    check(reconcile(rows, bank_cents=None) == ("NO_RECEIPT", None))
    check(reconcile(rows, bank_cents=18000, bank_group="G2")[0] == "UNMATCHED")
    check(reconcile(rows, bank_cents=18000, bank_currency="USD")[0] == "UNMATCHED")
    check(reconcile(rows, bank_cents=18000, bank_seller="S2")[0] == "UNMATCHED")
    check(reconcile(rows, complete=False, bank_cents=18000)[0] == "WAIT")
    check(reconcile(rows, closed=False, bank_cents=18000)[0] == "WAIT")
    check(reconcile([r for r in rows if r.transaction != "T3"])[0] == "GROUP_GAP")
    check(rejects(lambda: latest(rows + [replace(rows[1], cents=13000)])))
    check(rejects(lambda: buckets(rows + [replace(T1, revision=3, status="UNKNOWN")], "S1", "EUR")))
    check(buckets(rows + [replace(T1, seller="S2", cents=9900)], "S1", "EUR") == expected)
    check(reconcile(rows + [replace(rows[1], revision=3, group="G1")])[0] == "REVIEW")
    # Témoins fautifs : addition de versions, mélange de devises, frais inventés.
    check(sum(r.cents for r in rows if r.currency == "EUR") == 62000 != 50000)
    check(sum(r.cents for r in latest(rows)) == 55000 != 50000)
    check(reconcile(rows)[0] != "MATCHED")  # 5 euros ne deviennent pas des frais.
    print(f"{checks} contrôles réussis ; différé 200 EUR ; libéré sans groupe 120 EUR ; groupe 180 EUR ; banque 175 EUR ; écart 5 EUR.")


if __name__ == "__main__":
    run()
