#!/usr/bin/env python3
"""Deterministic teaching models, NOT a MySQL engine or measurement.
Run: python3 labs/mysql/models.py index|mvcc|recovery|all
"""
import argparse
import bisect
import json


def index_model():
    # The input follows init.sql. Verify against independently worked examples.
    orders = [(n, n % 10000, ('NEW', 'PAID', 'SHIPPED', 'CANCELLED')[n % 4])
              for n in range(1, 100001)]
    empty = [n for n, user, status in orders if user == 1234 and status == 'PAID']
    hits = [n for n, user, status in orders if user == 1233 and status == 'PAID']
    assert empty == []
    assert hits == [1233, 11233, 21233, 31233, 41233, 51233, 61233, 71233, 81233, 91233]
    ids = [n for n, _, _ in orders]
    consumed = 0
    page = []
    for row in ids:
        consumed += 1
        if consumed > 90000:
            page.append(row)
        if len(page) == 20:
            break
    cursor = bisect.bisect_right(ids, 90000)
    keyset = ids[cursor:cursor + 20]
    assert page == keyset == list(range(90001, 90021))
    assert consumed == 90020
    # Ordered tie-breaker keeps same-time records on successive pages.
    records = [(10, 1), (10, 2), (11, 3), (11, 4)]
    assert [r for r in records if r > (10, 1)][:2] == [(10, 2), (11, 3)]
    assert [r for r in records if r[0] > 10][:2] == [(11, 3), (11, 4)]  # misses (10,2)
    return {'empty_count': len(empty), 'hit_count': len(hits),
            'offset_consumed': consumed, 'keyset_consumed': len(keyset),
            'same_time_without_id_misses': [10, 2]}


def mvcc_model():
    # A fixed ReadView; later commit does not mutate its active-id snapshot.
    creator, active, future = 70, frozenset({60, 80}), 90
    oldest = min(active)

    def visible(trx):
        return (trx == creator or trx < oldest or
                (trx < future and trx not in active))

    ids = [40, 60, 70, 75, 80, 95]
    seen = [trx for trx in ids if visible(trx)]
    assert seen == [40, 70, 75]
    # Newest first: a future row, active row, committed-before-view row.
    versions = [(95, 30), (80, 60), (75, 80), (40, 100)]
    snapshot_balance = next(value for trx, value in versions if visible(trx))
    own_balance = next(value for trx, value in [(70, 70)] + versions if visible(trx))
    assert (snapshot_balance, own_balance) == (80, 70)
    # BEGIN alone is not the first read. If B already committed, first RR read sees B.
    state = {'balance': 100}
    state['balance'] = 80
    rr_first_view = dict(state)
    state['balance'] = 60
    assert (rr_first_view['balance'], state['balance']) == (80, 60)
    # Write skew: disjoint writes both pass their independent old count.
    doctors = [1, 1]
    a_snapshot, b_snapshot = sum(doctors), sum(doctors)
    if a_snapshot > 1:
        doctors[0] = 0
    if b_snapshot > 1:
        doctors[1] = 0
    assert doctors == [0, 0]
    # Guard protocol model serializes check+write on CURRENT state.
    guarded = [1, 1]
    for doctor in [0, 1]:
        if sum(guarded) > 1:
            guarded[doctor] = 0
    assert guarded == [0, 1]
    return {'visible_ids': seen, 'snapshot_balance': snapshot_balance,
            'own_balance': own_balance, 'unguarded_on_call': sum(doctors),
            'guarded_on_call': sum(guarded)}


def recovery_model():
    # Wait graph: one edge is a wait, a cycle is deadlock. No victim policy is modeled.
    edges = {'A': 'B', 'B': 'A'}

    def cyclic(graph, start):
        visited = set()
        node = start
        while node in graph:
            if node in visited:
                return True
            visited.add(node)
            node = graph[node]
        return False

    assert cyclic(edges, 'A')
    assert not cyclic({'A': 'B'}, 'A')
    del edges['B']  # victim's waiting edge removed; its held locks must also be released in a DB
    assert not cyclic(edges, 'A')
    # A checkpoint is not a source version. Reject a stale backfill after an online update.
    row = {'version': 3, 'cents': 100, 'new_amount': None}
    captured = dict(row)
    row.update(version=4, cents=200, new_amount=2)
    affected = 0
    if row['version'] == captured['version'] and row['new_amount'] is None:
        row['new_amount'] = captured['cents'] / 100
        affected = 1
    assert (affected, row['new_amount'], row['version']) == (0, 2, 4)
    # Equal counts cannot detect exchanged values.
    expected = {1: (4, 200), 2: (1, 100)}
    corrupt = {1: (4, 100), 2: (1, 200)}
    assert len(expected) == len(corrupt) and expected != corrupt
    return {'wait_is_cycle': False, 'deadlock_is_cycle': True,
            'stale_backfill_affected': affected, 'current_cents': row['cents'],
            'backfill_min_seconds': 500000000 // 2000,
            'equal_counts_can_hide_corruption': True}


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('mode', choices=['index', 'mvcc', 'recovery', 'all'])
    mode = parser.parse_args().mode
    scenarios = {'index': index_model, 'mvcc': mvcc_model, 'recovery': recovery_model}
    result = {name: fn() for name, fn in scenarios.items() if mode in ('all', name)}
    print(json.dumps({'kind': 'teaching-model-not-mysql', 'results': result}, ensure_ascii=False, indent=2))
