#!/usr/bin/env python3
"""Offline sealed receipt verification; no network or business database access."""
import hashlib
import json
from pathlib import Path
import subprocess
import sys
import integrate_20261010 as tool
from integrate_20261011_batches46_48 import item

HERE = Path(__file__).resolve().parent
MIRROR = Path('/Users/wiscourper/Documents/workspace/local/dm8-sealed-ai-20261013.4PoFa9')
STORE = Path('/Users/wiscourper/Documents/workspace/local/dm8-store-purchase-read3-20261012.L5dTYR/stage3/cater-basic221-20261013')
PHP = '/opt/homebrew/opt/php@7.4/bin/php'
BASES = {
    221: tool.STORE + 'parallel-cater-basic-20261013/',
    223: tool.AI + 'parallel-order-comment-20261013/',
    224: tool.AI + 'parallel-terminal-dish-noop-20261013/',
    226: tool.AI + 'parallel-backend-logout-20261013/',
}


def pin(root, files):
    for name, digest in files.items():
        assert hashlib.sha256((root / name).read_bytes()).hexdigest() == digest, name


def load(root, name):
    return json.loads((root / name).read_text())


def events(root, number, lane):
    return {e['stage']: e for e in load(root, f'run{number}-{lane}.json')['events']}


def shape(value):
    if isinstance(value, dict):
        return {k: shape(v) for k, v in value.items()}
    if isinstance(value, list):
        return [shape(v) for v in value]
    return type(value).__name__


def wire(event):
    raw = event['response_raw_exact']
    assert hashlib.sha256(raw.encode()).hexdigest() == event['response_sha256']
    assert event['http'] == 200 and event['response_complete'] is True
    body = json.loads(raw)
    assert isinstance(body, dict) and type(body['status']) is int and body['status'] == 200
    body.pop('trace_id')
    return body


def leaves(value, path=''):
    if isinstance(value, dict):
        result = {}
        for key, child in value.items():
            result.update(leaves(child, path + '.' + key if path else key))
        return result
    if isinstance(value, list):
        result = {}
        for key, child in enumerate(value):
            result.update(leaves(child, path + '.' + str(key)))
        return result
    return {path: value}


def verify221():
    pin(STORE, {'REPORT221.md': '754d58da21a9aefaaf568518fdff12dc4840ea162b0fe7205361e5c140812d30',
        'manifest221.json': '8a930facde79551ba921c16dcdfe73cd307b69e260585c64a1c8ec029b2208c5',
        'verify221.py': 'b8a4ddab60c351069e0eedf99ccd70b337242de1241375e7d715280b6fdaf90d'})
    r = json.loads(subprocess.check_output([sys.executable, '-B', str(STORE / 'verify221.py')]))
    assert r['checks'] == 726


def verify223():
    root = MIRROR / 'meal223-order-comment'
    pin(root, {'REPORT_223.md': '6ca68cab2cfa8a86746c028154b34a8d1befa19394db3b6f70b89152acfb5260',
        'run223-B.json': 'ef08854fbf8496f79294bd3185455b609e7fc021bf850b2853901a0aed784c3e',
        'run223-C.json': 'e14b760f66d347af114a9ef3f69744701b372d3ead8248888cb35e0b7ab3fced',
        'verify223_offline.php': '5a564820746383d355e2006a8916ccbc7fade5970e991226a5370cd5ba4c8fcb',
        'verify223_fullvalue.php': '5d018bb45933beaa0c84f2c526135ca43b9804b607fe98151989916fa21e60a0'})
    for name in ('verify223_offline.php', 'verify223_fullvalue.php'):
        subprocess.run([PHP, str(root / name)], check=True, capture_output=True)
    bodies = {}
    for lane in ('B', 'C'):
        e = events(root, 223, lane)
        before, after = e['before_http'], e['comment_submit_readback']
        for table, count in [('new_order', 37), ('new_order_goods', 33)]:
            a, b = before[table + '_before'][0], after[table][0]
            assert len(a) == len(b) == count and set(a) == set(b)
            changes = {k for k in a if type(a[k]) is not type(b[k]) or a[k] != b[k]}
            assert 'is_comment' in changes and changes <= {'is_comment', 'update_time'}
            assert a['is_comment'] == 0 and b['is_comment'] == 1
        assert before['protected_hashes'] == after['protected_hashes_after'] == e['final_audit']['protected_hashes_final']
        for stage, method, path in [('express_http', 'GET', '/api/order/express'),
                ('comment_list_http', 'GET', '/api/order.comment/list'),
                ('comment_submit_http', 'POST', '/api/order.comment/submit')]:
            assert e[stage]['method'] == method and e[stage]['path'].split('?')[0] == path
            bodies[lane, stage] = wire(e[stage])
        form = json.loads(e['comment_submit_http']['request_body_raw'])['form']
        assert isinstance(form, list) and len(form) == 1 and form[0]['uploaded'] == []
        assert type(form[0]['uploaded']) is list
        assert after['comment'][0]['content'] == form[0]['content'] and '🧪' in form[0]['content']
        assert bodies[lane, 'comment_submit_http']['data'] == [] and type(bodies[lane, 'comment_submit_http']['data']) is list
    allowed = {
        'express_http': {'data.express.0.order_id', 'data.express.0.goods.0.order_id',
            'data.express.0.goods.0.order_goods_id', 'data.express.0.goods.0.goods_id',
            *{'data.express.0.goods.0.goods.' + k for k in ('order_goods_id', 'goods_id', 'goods_name', 'goods_no', 'order_id', 'user_id')}},
        'comment_list_http': {'data.goodsList.0.' + k for k in ('order_goods_id', 'goods_id', 'goods_name', 'goods_no', 'order_id', 'user_id')},
        'comment_submit_http': set(),
    }
    for stage, differences in allowed.items():
        a, b = bodies['B', stage], bodies['C', stage]
        assert shape(a) == shape(b)
        x, y = leaves(a), leaves(b)
        assert set(x) == set(y)
        assert {k for k in x if x[k] != y[k]} == differences


def verify_limited(number, directory, report, verifier, b_sha, c_sha):
    root = MIRROR / directory
    pin(root, {f'REPORT_{number}.md': report, f'verify{number}_offline.php': verifier,
        f'run{number}-B.json': b_sha, f'run{number}-C.json': c_sha})
    subprocess.run([PHP, str(root / f'verify{number}_offline.php')], check=True, capture_output=True)
    stages = ('edit_http', 'delete_http') if number == 224 else ('store_http', 'terminal_http')
    for stage in stages:
        a, b = [wire(events(root, number, lane)[stage]) for lane in ('B', 'C')]
        assert shape(a) == shape(b) and a == b
        assert type(a['data']) is list and a['data'] == []


def verify224():
    verify_limited(224, 'meal224-terminal-dish-noop',
        '6c4c0888de883a52f7308f894ac3b5343ba84ddef0b33fbec365740a98fa158e',
        '63cb7ab6b4c3156876df5bc47434c34c7c046cfa7a3e1feab462f1d0f2bd9195',
        'a2fb6bd3a546d9f9097f578c9ef45d74b30e234292a1b01b73bd6c6b11d20804',
        'dcbc03c6302a51426fafde35148c39e91a9528c786549bc74f5e5f27e820b59f')


def verify226():
    verify_limited(226, 'meal226-backend-logout',
        '8af52d7a87630dc7b211746677f16f853f21de87906abcdd3cbb6f6bcf839fb4',
        '65c30340dfb7fb5d00c3b01574e792949aa3b6f3b4d11fa6e6776bbf7e4ef477',
        '49aeebd77dd2a54d11a7e5135abedb7626bd1720084a3155cb76ad273c203fda',
        '67a7db766f3162840cb3c8241314ddb7ef52015d9b9efa5236338f6839b20c96')


def batches():
    return [
        (221, [item('store', 'POST', '/p/cater/update', BASES[221],
            success_case='partial-evidence-original-flag1-basic-edit-and-required-cook-method-rejection',
            readback='tested-pass-full84-only-four-original-fields-changed-six-recipes-unchanged')], verify221),
        (223, [item('ai_api', method, path, BASES[223],
            success_case='partial-evidence-owned-express-and-comment-positive-not-real-paid-receipt',
            readback='tested-pass-full37-33-own-rows-fourbyte-comment-and-protected-existing-rows')
            for method, path in [('GET', '/api/order/express'), ('GET', '/api/order.comment/list'),
                ('POST', '/api/order.comment/submit')]], verify223),
        (224, [item('ai_api', 'POST', '/terminal/dishes.Dishes/' + action, BASES[224],
            mysql_state='partial-evidence-common-source-TODO-read-noop', dm_state='partial-evidence-common-source-TODO-read-noop',
            success_case='partial-evidence-original-read-and-response-only-no-edit-delete-persistence',
            mysql_readback='partial-evidence-read-noop-unchanged-own-row', dm_readback='partial-evidence-read-noop-unchanged-own-row',
            readback='partial-evidence-fullrow-unchanged-original-TODO-not-write-acceptance')
            for action in ('edit', 'delete')], verify224),
        (226, [item('ai_api', 'POST', '/' + app + '/passport/logout', BASES[226], scope='no-db',
            mysql_state='partial-evidence-cache-contract-only', dm_state='partial-evidence-cache-contract-only',
            success_case='partial-evidence-cache-token-one-to-zero-no-database-coverage', readback='not-applicable',
            mysql_readback='not-applicable', dm_readback='not-applicable')
            for app in ('store', 'terminal')], verify226),
    ]


def verify():
    for _, _, check in batches():
        check()


if __name__ == '__main__':
    plans = batches()
    for number, _, check in plans:
        assert not (HERE / f'integration-20261013-batch{number}.json').exists(), 'Never replay integration main'
        check()
    for number, specs, check in plans:
        tool.main(specs, HERE / f'integration-20261013-batch{number}.json', check)
