"""Run both teaching experiments using Python standard-library modules only."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import platform
import sqlite3
import sys
import tempfile

sys.dont_write_bytecode = True
import access
import migration

ROOT = Path(__file__).resolve().parent
INPUTS = ['access.py', 'migration.py', 'run.py', 'requirements.json', 'cases.json', 'disposition.json', 'README.md']


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def mutation_run(cases):
    variants = {'baseline': access.baseline, 'M1_expiry_inclusive': access.expiry_inclusive,
                'M2_removed_revocation': access.removed_revocation}
    results = []
    for name, function in variants.items():
        for suite, selected in [('weak', cases[:1]), ('requirement_derived', cases)]:
            observations = []
            for case in selected:
                actual = function(case['active'], case['now'], case['expires_at'])
                observations.append({**case, 'actual': actual, 'passed': actual is case['expected']})
            passed = all(item['passed'] for item in observations)
            results.append({'variant': name, 'suite': suite, 'observations': observations,
                            'state': ('baseline_passed' if passed else 'baseline_failed') if name == 'baseline'
                            else ('survived' if passed else 'killed')})
    expected = ['baseline_passed', 'baseline_passed', 'survived', 'killed', 'survived', 'killed']
    if [item['state'] for item in results] != expected:
        raise RuntimeError('Mutation teaching outcomes changed; inspect all observations.')
    return {'method': 'two explicitly hand-applied source mutations, no Stryker or Proof execution',
            'results': results, 'hand_applied_mutants': 2, 'assertion_observations': 12,
            'weak_survivors': 2, 'requirement_derived_kills': 2,
            'limit': 'This selected mutant set and these three cases do not establish complete test adequacy.'}


def migration_run(cases):
    results = []
    with tempfile.TemporaryDirectory(prefix='proof-evidence-methods-') as scratch:
        for case in cases:
            old = migration.observe(Path(scratch) / (case['id'] + '-old.sqlite'), migration.old_api,
                                    case, 'old-' + case['id'])
            new = migration.observe(Path(scratch) / (case['id'] + '-new.sqlite'), migration.candidate_api,
                                    case, 'new-' + case['id'])
            # Check the field contract before accepting the permitted normalization.
            ids_valid = all(isinstance(obs['response'].get('request_id'), str) and
                            bool(obs['response']['request_id']) for obs in [old, new])
            old_pass = old['response']['status'] == case['expected_status'] and old['durable_rows'] == case['expected_rows']
            new_pass = new['response']['status'] == case['expected_status'] and new['durable_rows'] == case['expected_rows']
            results.append({'case': case, 'old': old, 'candidate': new, 'request_ids_valid': ids_valid,
                            'raw_equal': old == new,
                            'permitted_normalized_equal': ids_valid and migration.permitted_normalization(old) == migration.permitted_normalization(new),
                            'dangerous_normalized_equal': migration.dangerous_normalization(old) == migration.dangerous_normalization(new),
                            'old_meets_requirement': old_pass, 'candidate_meets_requirement': new_pass})
    if not (all(row['old_meets_requirement'] and row['request_ids_valid'] and not row['raw_equal'] for row in results)
            and [row['candidate_meets_requirement'] for row in results] == [True, False]
            and [row['permitted_normalized_equal'] for row in results] == [True, False]
            and all(row['dangerous_normalized_equal'] for row in results)):
        raise RuntimeError('Migration teaching outcomes changed; inspect the outputs and stored rows.')
    return {'method': 'four local API calls; committed SQLite state reread through fresh connections',
            'results': results, 'api_executions': 4, 'candidate_requirement_failures': 1,
            'dangerous_normalization_false_agreements': 1,
            'candidate_accepted': False,
            'disposition_kind': 'illustrative contract decision from disposition.json, not a real release approval',
            'limit': 'No deployed service, migration runner, rollback, concurrency or production database was tested.'}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output', default='results', help='New or empty output directory, relative to current working directory')
    args = parser.parse_args()
    output = Path(args.output).resolve()
    if output.exists() and (not output.is_dir() or any(output.iterdir())):
        parser.error('Output must be new or empty; retained results are never overwritten.')
    cases = json.loads((ROOT / 'cases.json').read_text())
    basis = {name: sha(ROOT / name) for name in INPUTS}
    report = {'schema': 'proof-evidence-methods-v1', 'kind': 'actual execution of synthetic teaching fixtures',
              'executed_at_utc': datetime.now(timezone.utc).isoformat(),
              'command': [sys.executable, *sys.argv], 'python': platform.python_version(),
              'platform': platform.system() + '/' + platform.machine(), 'sqlite': sqlite3.sqlite_version,
              'source_sha256': basis,
              'basis_sha256': hashlib.sha256(json.dumps(basis, sort_keys=True).encode()).hexdigest(),
              'mutation': mutation_run(cases['mutation']), 'migration': migration_run(cases['migration'])}
    output.mkdir(parents=True, exist_ok=True)
    target = output / 'results.json'
    target.write_text(json.dumps(report, indent=2) + '\n')
    summary = ('mutation: 2 hand-applied mutants; weak suite 2 survived; requirement-derived suite 2 killed\n'
               'migration: 4 API calls; candidate failed 1 of 2 requirement cases; dangerous comparison concealed 1 failure\n'
               'runner: exit 0 means the teaching outcomes matched, not that the migration candidate passed\n')
    (output / 'stdout.txt').write_text(summary)
    files = ['results.json', 'stdout.txt']
    (output / 'SHA256SUMS').write_text(''.join(sha(output / name) + '  ' + name + '\n' for name in files))
    print(summary, end='')


if __name__ == '__main__':
    main()
