import io, subprocess, sys

ROOT = r'C:\Users\decid\Documents\projects\spt-pacer-tool'
FRAME = ROOT + r'\src\frame.rs'
DIGEST = ROOT + r'\src\digest.rs'
MAIN = ROOT + r'\src\main.rs'
NL = chr(10)

MUTATIONS = [
    (FRAME, 'P1 parse_boundary skips MAC verification (accepts an unstamped line)',
     '    let (open_tag, _body) = decode_frame(line, link)?;' + NL
     + '    if attr_of(&open_tag, "type").as_deref() != Some("boundary") {',
     '    let (open_tag, _body) = decode_frame_UNSAFE(line)?;' + NL
     + '    if attr_of(&open_tag, "type").as_deref() != Some("boundary") {',
     ['boundary_frame_with_a_bad_mac_is_refused'],
     NL + '''fn decode_frame_UNSAFE(line: &str) -> Option<(String, String)> {
    let frame = line.split_once(' ').map(|(_, f)| f).unwrap_or(line);
    let open_end = frame.find('>')?;
    let open_tag = frame[..=open_end].to_string();
    let rest = &frame[open_end + 1..];
    let body = rest.strip_suffix("</EVENT>").unwrap_or(rest).to_string();
    Some((open_tag, body))
}
'''),
    (FRAME, 'P2 parse_boundary claims io frames too (drops the type check)',
     '    if attr_of(&open_tag, "type").as_deref() != Some("boundary") {' + NL + '        return None;' + NL + '    }',
     '',
     ['the_boundary_and_io_decoders_do_not_claim_each_other'],
     ''),
    (MAIN, 'P4 a refused boundary is dropped silently instead of counted',
     '        None if line.contains("type=\\"boundary\\"") => BoundaryLine::RefusedClaim,',
     '        None if false => BoundaryLine::RefusedClaim,',
     ['a_boundary_that_fails_its_mac_is_refused_and_counted'],
     ''),
    (MAIN, 'P5 a decoded boundary no longer arms education',
     '        Some(_) => BoundaryLine::Decoded,',
     '        Some(_) => BoundaryLine::NotOurs,',
     ['several_boundaries_in_one_drain_educate_once'],
     ''),
]


def run():
    r = subprocess.run(['cargo', 'test'], cwd=ROOT, capture_output=True, text=True, errors='replace')
    return r.stdout + r.stderr


def failing(out):
    n = set()
    for line in out.splitlines():
        line = line.strip()
        if line.startswith('---- ') and line.endswith(' stdout ----'):
            n.add(line[5:-12].strip().split('::')[-1])
    return n


ok = True
for path, name, anchor, repl, must_fail, extra in MUTATIONS:
    orig = io.open(path, encoding='utf-8').read()
    c = orig.count(anchor)
    if c != 1:
        print('BAD ANCHOR %-56s count=%d' % (name, c))
        ok = False
        continue
    try:
        io.open(path, 'w', encoding='utf-8', newline=NL).write(orig.replace(anchor, repl, 1) + extra)
        out = run()
        if 'could not compile' in out or 'error[E' in out:
            print('DID NOT COMPILE %-51s' % name)
            for l in out.splitlines():
                if l.startswith('error'):
                    print('    ', l)
            ok = False
            continue
        f = failing(out)
        missed = [t for t in must_fail if t not in f]
        print('%s %-54s failed=%s' % ('CAUGHT ' if not missed else 'ESCAPED', name, sorted(f) or 'NONE'))
        if missed:
            print('         did NOT fail and should have:', missed)
            ok = False
    finally:
        io.open(path, 'w', encoding='utf-8', newline=NL).write(orig)

print()
print('ALL MUTATIONS CAUGHT' if ok else 'SOME ESCAPED OR MISAPPLIED')
sys.exit(0 if ok else 1)
