"""Disposable, sequential SQLite model of two local transaction boundaries.
No broker, network, external warehouse API, concurrency or throughput is tested.
"""
import sqlite3
import json


def databases():
    sender = sqlite3.connect(':memory:')
    receiver = sqlite3.connect(':memory:')
    sender.executescript('CREATE TABLE orders(id TEXT PRIMARY KEY); CREATE TABLE outbox(id TEXT PRIMARY KEY, order_id TEXT NOT NULL, sent INTEGER NOT NULL DEFAULT 0);')
    receiver.executescript('CREATE TABLE inbox(id TEXT PRIMARY KEY); CREATE TABLE jobs(order_id TEXT PRIMARY KEY, message_id TEXT NOT NULL);')
    return sender, receiver


def save(sender, fail=False):
    try:
        with sender:
            sender.execute('INSERT INTO orders VALUES (?)', ('O42',))
            sender.execute('INSERT INTO outbox(id,order_id) VALUES (?,?)', ('M7','O42'))
            if fail:
                raise RuntimeError('injected before sender commit')
    except RuntimeError:
        pass


def receive(receiver, message_id='M7', order_id='O42', fail=False):
    try:
        with receiver:
            inserted = receiver.execute('INSERT OR IGNORE INTO inbox VALUES (?)', (message_id,)).rowcount
            if inserted:
                receiver.execute('INSERT INTO jobs VALUES (?,?)', (order_id,message_id))
            if fail:
                raise RuntimeError('injected before receiver commit')
    except RuntimeError:
        return 'not acknowledged'
    return 'acknowledged'


def state(sender, receiver):
    return {'orders':sender.execute('SELECT count(*) FROM orders').fetchone()[0],
            'pending':sender.execute('SELECT count(*) FROM outbox WHERE sent=0').fetchone()[0],
            'inbox':receiver.execute('SELECT count(*) FROM inbox').fetchone()[0],
            'jobs':receiver.execute('SELECT count(*) FROM jobs').fetchone()[0]}


def run():
    rows=[]
    s,r=databases();save(s,True)
    assert state(s,r)=={'orders':0,'pending':0,'inbox':0,'jobs':0}
    rows.append({'point':'sender rolls back',**state(s,r)})
    save(s);rows.append({'point':'sender committed, publisher not run',**state(s,r)})
    assert receive(r,fail=True)=='not acknowledged'
    assert state(s,r)=={'orders':1,'pending':1,'inbox':0,'jobs':0}
    rows.append({'point':'receiver rolls back',**state(s,r)})
    receive(r) # Simulate loss of acknowledgement: do not mark sender sent.
    assert state(s,r)=={'orders':1,'pending':1,'inbox':1,'jobs':1}
    rows.append({'point':'receiver committed, acknowledgement lost',**state(s,r)})
    receive(r) # Same message, not a new identity.
    with s:s.execute('UPDATE outbox SET sent=1 WHERE id=?',('M7',))
    assert state(s,r)=={'orders':1,'pending':0,'inbox':1,'jobs':1}
    rows.append({'point':'redelivery acknowledged',**state(s,r)})
    a,b=databases()
    with a:a.execute('INSERT INTO orders VALUES (?)',('O42',))
    # Crash here: there is no saved publication obligation.
    assert state(a,b)=={'orders':1,'pending':0,'inbox':0,'jobs':0}
    rows.append({'point':'negative control: save first, no outbox',**state(a,b)})
    c,d=databases();receive(d) # Sender subsequently never commits.
    assert state(c,d)=={'orders':0,'pending':0,'inbox':1,'jobs':1}
    rows.append({'point':'negative control: send first',**state(c,d)})
    return rows

if __name__=='__main__':print(json.dumps(run(),indent=2))
