"""Démonstration locale d'un index trié sous une ancienne règle.
Ni PostgreSQL, ni ICU, ni libc : deux ordres pédagogiques explicites.
Aucun serveur, aucun fichier de base et aucune connexion réseau.
"""
from dataclasses import dataclass
from hashlib import sha256
import json

ROWS = ((1, 'Aster'), (2, 'Écume'), (3, 'Orme'), (4, 'Zèbre'))
ORDERS = {'v1': {'Aster': 0, 'Orme': 1, 'Zèbre': 2, 'Écume': 3},
          'v2': {'Aster': 0, 'Écume': 1, 'Orme': 2, 'Zèbre': 3}}


def digest(rows):
    return sha256(json.dumps(sorted(rows), ensure_ascii=False).encode()).hexdigest()


@dataclass
class Index:
    entries: tuple
    recorded: str

    @classmethod
    def build(cls, rows, version):
        return cls(tuple(sorted(rows, key=lambda row: ORDERS[version][row[1]])), version)

    def refresh(self, version):
        self.recorded = version  # ne touche volontairement pas aux entrées

    def ordered(self, version):
        ranks = [ORDERS[version][row[1]] for row in self.entries]
        return ranks == sorted(ranks)

    def find(self, label, version):
        target = ORDERS[version][label]
        lo, hi = 0, len(self.entries)
        while lo < hi:
            mid = (lo + hi) // 2
            if ORDERS[version][self.entries[mid][1]] < target:
                lo = mid + 1
            else:
                hi = mid
        return self.entries[lo][0] if lo < len(self.entries) and self.entries[lo][1] == label else None


def receivable(index, runtime):
    return (index.recorded == runtime and index.ordered(runtime)
            and all(index.find(label, runtime) == identity for identity, label in ROWS)
            and digest(index.entries) == digest(ROWS))


def main():
    checks = 0
    def check(value):
        nonlocal checks
        assert value
        checks += 1
    old = Index.build(ROWS, 'v1')
    check([r[1] for r in old.entries] == ['Aster', 'Orme', 'Zèbre', 'Écume'])
    check(old.find('Écume', 'v1') == 2)
    check(old.find('Écume', 'v2') is None)
    check(next(i for i, label in ROWS if label == 'Écume') == 2)
    check(len(old.entries) == len(ROWS) == 4)
    check(digest(old.entries) == digest(ROWS))
    check(not old.ordered('v2'))
    entries_before = old.entries
    old.refresh('v2')
    check(old.recorded == 'v2')
    check(old.entries == entries_before)
    check(old.find('Écume', 'v2') is None)
    check(not receivable(old, 'v2'))
    rebuilt = Index.build(ROWS, 'v2')
    check([r[1] for r in rebuilt.entries] == ['Aster', 'Écume', 'Orme', 'Zèbre'])
    for identity, label in ROWS:
        check(rebuilt.find(label, 'v2') == identity)
    check(digest(rebuilt.entries) == digest(old.entries))
    check(receivable(rebuilt, 'v2'))
    # Une copie reconstruite ne valide pas une autre copie restée ancienne.
    replica = Index(entries_before, 'v2')
    check(not all(receivable(x, 'v2') for x in (rebuilt, replica)))
    # Un sous-ensemble peut garder le même ordre : un témoin limité peut passer.
    unchanged = Index.build(tuple(r for r in ROWS if r[1] != 'Écume'), 'v1')
    check(unchanged.ordered('v2'))
    # Trois critères de clôture volontairement insuffisants.
    check(len(old.entries) == len(ROWS) and not receivable(old, 'v2'))
    check(digest(old.entries) == digest(ROWS) and not receivable(old, 'v2'))
    check(old.recorded == 'v2' and not receivable(old, 'v2'))
    print(f'{checks} contrôles réussis ; 3 clôtures insuffisantes détectées')
    print('Ancien ordre : Aster, Orme, Zèbre, Écume ; recherche Écume sous v2 : absente')
    print('Après refresh seul : versions égales, recherche toujours incorrecte')
    print('Après reconstruction : Aster, Écume, Orme, Zèbre ; quatre identités retrouvées')
    print('Les deux ordres sont fictifs ; aucun PostgreSQL ou fournisseur de collation exécuté.')


if __name__ == '__main__':
    main()
