"""Compile and run the reviewed educational examples; no package installation."""
from pathlib import Path
import argparse
import hashlib
import json
import os
import subprocess
import time

parser = argparse.ArgumentParser()
parser.add_argument('--cxx', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--sanitize', action='store_true')
parser.add_argument('--run-as')
args = parser.parse_args()
source = Path(__file__).resolve().parent
output = Path(args.output).resolve()
assert output != source and not source.is_relative_to(output), 'Output must not replace source'
assert not output.exists(), 'Use a fresh output path to retain prior evidence'
output.mkdir(parents=True)
temporary = output / 'tmp'
temporary.mkdir()
environment = dict(os.environ, TMP=str(temporary), TEMP=str(temporary), TMPDIR=str(temporary),
                   ASAN_OPTIONS='detect_leaks=1:alloc_dealloc_mismatch=1:halt_on_error=1',
                   UBSAN_OPTIONS='halt_on_error=1:print_stacktrace=1', PYTHONDONTWRITEBYTECODE='1')
environment['PATH'] = str(Path(args.cxx).parent) + os.pathsep + environment.get('PATH', '')
report = {'compiler': args.cxx, 'sanitize': args.sanitize, 'source_files': {}, 'checks': []}
for path in sorted(source.iterdir()):
    if path.is_file(): report['source_files'][path.name] = hashlib.sha256(path.read_bytes()).hexdigest()

def save():
    (output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')

def run(label, command, stdin='', expected_code=0, expected_stdout=None, error_contains=None):
    start = time.monotonic()
    try:
        result = subprocess.run(command, cwd=source, env=environment, input=stdin,
                                text=True, capture_output=True, timeout=90 if label.startswith('compile') else 30)
    except subprocess.TimeoutExpired as exc:
        stdout = exc.stdout or ''
        stderr = exc.stderr or ''
        if isinstance(stdout, bytes): stdout = stdout.decode('utf-8', errors='replace')
        if isinstance(stderr, bytes): stderr = stderr.decode('utf-8', errors='replace')
        result = subprocess.CompletedProcess(command, 124, stdout, stderr + '\nCHECK TIMEOUT\n')
    (output / (label + '.stdout.txt')).write_text(result.stdout, encoding='utf-8')
    (output / (label + '.stderr.txt')).write_text(result.stderr, encoding='utf-8')
    passed = result.returncode == expected_code
    if expected_stdout is not None: passed &= result.stdout == expected_stdout
    if error_contains is not None: passed &= error_contains in result.stderr
    if args.sanitize:
        passed &= not any(marker in result.stderr for marker in
                          ('AddressSanitizer', 'LeakSanitizer', 'runtime error:', 'DEADLYSIGNAL'))
    report['checks'].append(dict(label=label, command=command, returncode=result.returncode,
                                  expected_returncode=expected_code, elapsed=time.monotonic()-start,
                                  passed=passed, stdin=stdin[:500], stdin_length=len(stdin)))
    save()
    if not passed:
        raise SystemExit(label + ' failed: ' + result.stdout + result.stderr)
    print(label + ': passed', flush=True)
    return result

flags = ['-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
         '-D_GLIBCXX_ASSERTIONS', '-fno-omit-frame-pointer']
if args.sanitize: flags += ['-fsanitize=address,undefined', '-fno-sanitize-recover=all']
run('compiler-version', [args.cxx, '--version'])
executables = {}
for name in ['tests', 'list_demo', 'applications_demo']:
    binary = output / (name + ('.exe' if os.name == 'nt' else ''))
    run('compile-' + name, [args.cxx, *flags, str(source / (name + '.cpp')), '-o', str(binary)])
    executables[name] = ([str(binary)] if not args.run_as else ['runuser', '-u', args.run_as, '--', str(binary)])
result = run('contract-and-random-tests', executables['tests'])
report['unit_summary'] = json.loads(result.stdout)
for mode, capacity in [('array', 512), ('single', 2), ('double', 2)]:
    run('list-' + mode, executables['list_demo'] + [mode], '+a+b\n#=c\n',
        expected_stdout=f'a b {{capacity = {capacity}, length = 2, cursor = 1, limit = 512}}\n'
                        f'c b {{capacity = {capacity}, length = 2, cursor = 0, limit = 512}}\n')
run('list-missing-argument', executables['list_demo'], '+\n', 2, '', 'missing character argument')
run('list-long-input', executables['list_demo'], ' ' * 150000 + '+a\n',
    expected_stdout='a {capacity = 512, length = 1, cursor = 0, limit = 512}\n')
app = executables['applications_demo']
run('window', app + ['window'], '8 3\n1 3 -1 -3 5 3 6 7\n',
    expected_stdout='-1 -3 -3 -3 3 3\n3 3 5 5 6 7\n')
run('window-zero', app + ['window'], '1 0\n2\n', 2, '', 'window must')
run('count-negative', app + ['quick'], '-1\n', 2, '', 'n must')
run('count-too-large', app + ['quick'], '200001\n', 2, '', 'n must')
run('missing-value', app + ['quick'], '2\n1\n', 2, '', 'missing integer')
run('invalid-integer', app + ['quick'], '1\nxyz\n', 2, '', 'invalid or out-of-range')
run('extra-value', app + ['quick'], '1\n2 3\n', 2, '', 'extra token')
run('quick-empty', app + ['quick'], '0\n', expected_stdout='\n')
run('quick', app + ['quick'], '6\n3 -2 3 0 2147483647 -2147483648\n',
    expected_stdout='-2147483648 -2 0 3 3 2147483647\n')
run('stock', app + ['stock'], '7\n100 80 60 70 60 75 85\n', expected_stdout='1 1 1 2 1 4 6\n')
run('stock-equal', app + ['stock'], '4\n2 2 2 2\n', expected_stdout='1 2 3 4\n')
run('radix-int', app + ['radix-int'], '100 0 10 18446744073709551615 10\n',
    expected_stdout='0 10 10 100 18446744073709551615\n')
run('radix-negative', app + ['radix-int'], '-1\n', 2, '', 'invalid or out-of-range')
run('radix-overflow', app + ['radix-int'], '18446744073709551616\n', 2, '', 'invalid or out-of-range')
run('radix-empty', app + ['radix-int'], '', expected_stdout='\n')
run('radix-string', app + ['radix-string'], 'zzzzzzzz AAAAAAAA aaaaaaaa AbCdEfGh\n',
    expected_stdout='AAAAAAAA AbCdEfGh aaaaaaaa zzzzzzzz\n')
run('radix-short', app + ['radix-string'], 'short\n', 2, '', 'eight ASCII letters')
run('radix-nonletter', app + ['radix-string'], 'abcd1234\n', 2, '', 'eight ASCII letters')
run('expression', app + ['expr'], '2^3^2\n2^-3\n-2^2\n.125+1.375\n',
    expected_stdout='512.000000\n0.125000\n-4.000000\n1.500000\n')
run('expression-left', app + ['expr'], '1+2)\n', 2, '', 'missing_left_parenthesis')
run('expression-right', app + ['expr'], '((1+2)\n', 2, '', 'missing_right_parenthesis')
run('expression-zero', app + ['expr'], '1/0\n', 2, '', 'division_by_zero')
run('expression-blank', app + ['expr'], '\n', 2, '', 'missing_operand')
run('expression-eof', app + ['expr'], '', 2, '', 'missing expression')
run('mode-invalid', app + ['unknown'], '', 2, '', 'unknown mode')
report['status'] = 'passed'
save()
print(json.dumps({'status':'passed', 'checks':len(report['checks']), 'unit_summary':report['unit_summary']}))
