"""Negative controls for W7 units. usage: mutate.py apply|restore
apply: back up each file, apply ONE mutation per targeted behaviour.
restore: copy backups back and assert byte identity with the pre-mutation sha."""
import hashlib, json, os, shutil, sys

ROOT = os.getcwd()
BK = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mut-backup')
MUTS = [
    # (file, old, new, the test expected to go red)
    ('crates/spt-daemon/src/autoapply.rs',
     'class == UpdateClass::BrainOnly && auto.contains(&AutoClass::from_update_class(class))',
     'auto.contains(&AutoClass::from_update_class(class))',
     'only_a_brain_only_class_inside_the_set_applies_unattended'),
    ('crates/spt-daemon/src/autoapply.rs',
     'if matches!(cache.applied_state(), Some(AppliedRecord::AppliedPending { .. })) {',
     'if false {',
     'the_watcher_waits_out_an_open_trial_and_skips_the_promoted_image'),
    ('crates/spt-daemon/src/autoapply.rs',
     '            if attempts >= MAX_ATTEMPTS {',
     '            if false {',
     'a_failed_apply_retries_then_settles'),
    ('crates/spt-daemon/src/autoapply.rs',
     '    let Some(AppliedRecord::Applied { version }) = applied else {\n        return None;\n    };',
     '    let version = match applied {\n        Some(AppliedRecord::Applied { version }) | Some(AppliedRecord::AppliedPending { version, .. }) => version,\n        _ => return None,\n    };',
     'the_adapters_leg_waits_for_the_promoted_swap'),
    ('crates/spt-daemon/src/config.rs',
     '        (None, Some(false)) => BTreeSet::new(),',
     '        (None, Some(false)) => default_auto_classes(),',
     'auto_classes_migrate_from_the_boolean_without_changing_its_meaning'),
    ('crates/spt-daemon/src/config.rs',
     '            full_auto_update: None,\n            auto_classes: Some(',
     '            full_auto_update: Some(self.auto_classes.is_empty()),\n            auto_classes: Some(',
     'a_config_round_trip_drops_the_deprecated_key'),
    ('crates/spt-daemon/src/pump/heartbeat.rs',
     '    period + jitter.min(RELEASE_CHECK_JITTER.saturating_sub(Duration::from_millis(1)))',
     '    period.saturating_sub(RELEASE_CHECK_JITTER / 2) + jitter',
     'the_delay_is_the_period_plus_a_bounded_additive_jitter'),
    ('crates/spt-daemon/src/applyhost.rs',
     '    swap_and_record(cache, exe_path, version, &artifact, prior_product_version)?;\n    // The supervisor consumes this',
     '    swap_and_record(cache, exe_path, version, &artifact, env!("CARGO_PKG_VERSION"))?;\n    // The supervisor consumes this',
     'apply_staged_in_broker_swaps_records_and_raises_the_signal_in_process'),
    ('crates/spt/src/cli.rs',
     '        BundleMemberAction::Install | BundleMemberAction::Upgrade if !allow_apply => {',
     '        BundleMemberAction::Install | BundleMemberAction::Upgrade if false && !allow_apply => {',
     'the_unattended_leg_offers_members_when_adapters_are_outside_the_auto_set'),
]


def sha(p):
    return hashlib.sha256(open(p, 'rb').read()).hexdigest()


if sys.argv[1] == 'apply':
    os.makedirs(BK, exist_ok=True)
    shas = {}
    for f, *_ in MUTS:
        if f not in shas:
            shas[f] = sha(f)
            shutil.copy2(f, os.path.join(BK, f.replace('/', '__')))
    json.dump(shas, open(os.path.join(BK, 'shas.json'), 'w'))
    for f, old, new, t in MUTS:
        s = open(f, encoding='utf-8').read()
        assert s.count(old) == 1, (f, old[:60])
        open(f, 'w', encoding='utf-8', newline='\n').write(s.replace(old, new))
    print('\n'.join(t for *_, t in MUTS))
else:
    shas = json.load(open(os.path.join(BK, 'shas.json')))
    for f, want in shas.items():
        shutil.copy2(os.path.join(BK, f.replace('/', '__')), f)
        assert sha(f) == want, f
    print('restored', len(shas), 'files, byte-identical')
