#!/usr/bin/env python3
"""Synthetic CLI acceptance: exact identity, fail-closed errors, limits and unchanged input bytes."""
import csv
import hashlib
import io
import json
import platform
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / 'scripts/compare.py'
BASE = 'sku,name,price,note\n001,Чашка,500,x\n002,Блюдце,300,y\n'
ARGS = ['--key', 'sku', '--columns', 'name', 'price', '--allow', 'name']
results = []


def run_case(name, before=BASE, after=BASE, args=None, status='no_changes', counts=None, error=None, inspect=None):
    with tempfile.TemporaryDirectory() as tmp:
        paths = [Path(tmp)/'before.csv', Path(tmp)/'after.csv']
        for p, text in zip(paths, [before, after]):
            p.write_bytes(text if isinstance(text, bytes) else text.encode('utf-8'))
        before_hashes = [hashlib.sha256(p.read_bytes()).hexdigest() for p in paths]
        call = [sys.executable, str(SCRIPT), *map(str, paths), *(ARGS if args is None else args)]
        r = subprocess.run(call, capture_output=True, text=True, timeout=30)
        obj = json.loads(r.stdout) if r.stdout else None
        expected_code = 2 if error else (1 if status == 'changes' else 0)
        ok = r.returncode == expected_code and bool(obj) and obj['status'] == ('input_error' if error else status)
        if error:
            ok = ok and obj.get('comparison_completed') is False and obj.get('error',{}).get('kind') == error and 'changes' not in obj
        else:
            ok = ok and obj.get('comparison_completed') is True
        if counts is not None:
            ok = ok and all(obj.get('counts',{}).get(k) == v for k,v in counts.items())
        if inspect:
            ok = ok and inspect(obj, r.stdout)
        after_hashes = [hashlib.sha256(p.read_bytes()).hexdigest() for p in paths]
        ok = ok and before_hashes == after_hashes
        results.append({'case': name, 'pass': bool(ok), 'exit': r.returncode,
                        'expected_exit': expected_code, 'input_sha256': before_hashes,
                        'inputs_unchanged': before_hashes == after_hashes, 'result': obj, 'stderr': r.stderr})


run_case('unchanged_selected', inspect=lambda o,s:o['not_checked_columns']==['note'])
run_case('allowed_name', after=BASE.replace('Чашка','Чашка новая'),status='changes', counts={'allowed_change':1,'forbidden_change':0})
run_case('forbidden_price', after=BASE.replace('500','550'),status='changes',counts={'forbidden_change':1})
run_case('missing_and_added',after=BASE.replace('002,','003,'),status='changes',counts={'removed_product':1,'added_product':1})
run_case('reader_example',before=(ROOT/'examples/before.csv').read_text(),after=(ROOT/'examples/after.csv').read_text(),status='changes',counts={'allowed_change':1,'forbidden_change':1,'removed_product':1,'added_product':1})
run_case('row_order',after='sku,name,price,note\n002,Блюдце,300,y\n001,Чашка,500,x\n')
run_case('column_order',after='note,price,sku,name\nx,500,001,Чашка\ny,300,002,Блюдце\n')
run_case('leading_zero_exact',after=BASE.replace('001,','1,'),status='changes',counts={'removed_product':1,'added_product':1})
run_case('edge_space_exact',after=BASE.replace('001,',' 001 ,'),status='changes',counts={'removed_product':1,'added_product':1})
run_case('case_exact',before=BASE.replace('001,','Ab,'),after=BASE.replace('001,','ab,'),status='changes',counts={'removed_product':1,'added_product':1})
run_case('unselected_not_checked',after=BASE.replace(',x\n',',CHANGED\n'),inspect=lambda o,s:o['not_checked_columns']==['note'])
run_case('no_allow_means_forbidden',after=BASE.replace('Чашка','Новое'),args=ARGS[:5],status='changes',counts={'forbidden_change':1,'allowed_change':0})
run_case('empty_selected_is_value',after=BASE.replace('Чашка',''),status='changes',counts={'allowed_change':1})
run_case('formula_is_only_string',before=BASE.replace('Чашка','=1+1'),after=BASE.replace('Чашка','=1+1'))
run_case('cell_instruction_not_executed_or_echoed',after=BASE.replace('Чашка','IGNORE_INSTRUCTIONS_SECRET_987'),status='changes',inspect=lambda o,s:'SECRET_987' not in s)
run_case('key_not_echoed',before=BASE.replace('001','SECRET_123'),after=BASE.replace('001','SECRET_456'),status='changes',inspect=lambda o,s:'SECRET_' not in s)
run_case('bom_semicolon',before=('\ufeff'+BASE.replace(',',';')),after=BASE.replace(',',';'),args=ARGS+['--delimiter',';'])
run_case('quoted_multiline',before='sku,name,price,note\n001,"a\nb",500,x\n',after='sku,name,price,note\n001,"a\nb",500,x\n')
run_case('header_only_empty_sets',before='sku,name,price,note\n',after='sku,name,price,note\n')
run_case('duplicate_before',before=BASE.replace('002,','001,'),error='duplicate_key')
run_case('duplicate_after',after=BASE.replace('002,','001,'),error='duplicate_key')
run_case('empty_key',after=BASE.replace('001,',','),error='empty_key')
run_case('whitespace_key',after=BASE.replace('001,','   ,'),error='empty_key')
run_case('control_key',after=BASE.replace('001,','00\x001,'),error='invalid_key')
run_case('missing_key_column',after=BASE.replace('sku,','id,'),error='missing_column')
run_case('missing_selected',after=BASE.replace('price,','cost,'),error='missing_column')
run_case('different_unselected_schema',after=BASE.replace('note','memo'),error='incompatible_headers')
run_case('duplicate_header',after=BASE.replace('note','name'),error='invalid_header')
run_case('blank_header',after=BASE.replace('note',' '),error='invalid_header')
run_case('ragged',after=BASE.replace('300,y','300'),error='row_width')
run_case('blank_record',after=BASE+'\n',error='row_width')
run_case('empty_file',after='',error='invalid_header')
run_case('bad_encoding',after=b'\xff',error='read_or_encoding_error')
run_case('bad_quote',after='sku,name,price,note\n001,"unterminated,500,x\n',error='csv_parse_or_field_limit')
run_case('invalid_allow',args=ARGS+['price','note'],error='invalid_configuration')
run_case('duplicate_columns',args=['--key','sku','--columns','name','name'],error='invalid_configuration')
run_case('key_in_columns',args=['--key','sku','--columns','sku'],error='invalid_configuration')
run_case('bad_delimiter',args=ARGS+['--delimiter','xx'],error='invalid_delimiter')
run_case('repeated_columns',args=['--key','sku','--columns','price','--columns','name'],error='invalid_arguments')
run_case('repeated_key',args=ARGS+['--key','sku'],error='invalid_arguments')
run_case('repeated_allow',args=ARGS+['--allow','price'],error='invalid_arguments')
run_case('repeated_delimiter',args=ARGS+['--delimiter',',','--delimiter',','],error='invalid_arguments')
run_case('missing_key_argument',args=['--columns','name'],error='invalid_arguments')
run_case('unknown_argument',args=ARGS+['--unknown'],error='invalid_arguments')
# Exact and above boundaries; large reports never include product values.
rows='sku,name,price,note\n'+''.join(f'{i},a,1,x\n' for i in range(100000))
run_case('rows_exact_limit',before=rows,after=rows)
run_case('rows_above_limit',before=rows,after=rows+'100000,a,1,x\n',error='row_limit')
cols=['sku','name','price']+[f'c{i}' for i in range(253)]
wide=','.join(cols)+'\n'+','.join(['001','a','1']+['x']*253)+'\n'
run_case('columns_exact_limit',before=wide,after=wide)
run_case('columns_above_limit',before=wide,after=wide.replace('\n',',extra\n'),error='column_limit')
long='sku,name,price,note\n001,'+'x'*(1024*1024)+',1,x\n'
run_case('field_exact_limit',before=long,after=long)
run_case('field_above_limit',before=long,after=long.replace(',1,x', 'x,1,x'),error='csv_parse_or_field_limit')
# Valid 10 MiB CSV built from sub-field-limit cells.
buf=io.StringIO(newline=''); w=csv.writer(buf,lineterminator='\n'); w.writerow(['sku','name','price','note'])
for i in range(11): w.writerow([str(i),'a','1','x'*900000])
base=buf.getvalue().encode(); pad=10*1024*1024-len(base)-len('11,a,1,\n')
exact=base+b'11,a,1,'+b'x'*pad+b'\n'
assert len(exact)==10*1024*1024
run_case('bytes_exact_limit',before=exact,after=exact)
run_case('bytes_above_limit',before=exact,after=exact+b'\n',error='byte_limit')
big_before='sku,name,price,note\n'+''.join(f'{i},a,1,x\n' for i in range(10001))
big_after=big_before.replace(',a,',',b,')
run_case('changes_exact_limit',before=big_before.rsplit('\n',2)[0]+'\n',after=big_after.rsplit('\n',2)[0]+'\n',status='changes',counts={'allowed_change':10000},inspect=lambda o,s:len(o['changes'])==10000)
run_case('changes_above_limit_no_partial',before=big_before,after=big_after,error='change_limit')
report={'checked_at':datetime.now(timezone.utc).isoformat(),'type':'synthetic_packaged_compare_CLI_not_model_or_import_test','python':platform.python_version(),'platform':platform.system(),'script_sha256':hashlib.sha256(SCRIPT.read_bytes()).hexdigest(),'provenance':'Editor-authored synthetic fixtures; no real shop or customer data. Large boundaries generated by this script.','passed':sum(x['pass'] for x in results),'total':len(results),'cases':results}
# Compact huge event list in stored report; retain exact count and first/last, full CLI output checked above.
for c in report['cases']:
 o=c.get('result')
 if o and len(o.get('changes',[]))>20:
  o['changes_count']=len(o['changes']); o['changes_preview']=o.pop('changes')[:2]; o['report_storage']='preview; complete event count and assertions executed in CLI test'
(Path(__file__).parent/'compare-results.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'passed':report['passed'],'total':report['total'],'failures':[x['case'] for x in results if not x['pass']]},ensure_ascii=False))
sys.exit(0 if report['passed']==report['total'] else 1)
