#!/usr/bin/env python3
"""Mutant study: does each oracle notice a change to the native rotation code?

Each mutant is one textual replacement applied identically to (a) the copy of
the native source that the 22 string-matching contract tests read, and (b) the
harness copy of the same function. 'behaviour' mutants change what the widget
shows; 'refactor' mutants are behaviour-preserving rewrites. A mutant is killed
by the contract tests if any of the 22 fails, and by the differential test if
any query answer changes against the unmutated run on the mutant-study zones.

Usage: mutants.py <root> list|apply <id> <tree>|harness <id> <dir>
"""
import sys, json, os

KT = "android/app/src/main/kotlin/com/adelanteapp/adelante/AdelanteWidgetProvider.kt"
SW = "ios/AdelanteWidgets/AdelanteWidgetShared.swift"
MUTANTS = [
    # id, platform, kind, old, new, description
    ("K1", "kotlin", "behaviour", "1 + ((steps - 1) % (pool.size - 1)) else steps % pool.size", "1 + ((steps - 1) % (pool.size - 1)) else (steps + 1) % pool.size", "daily path off by one"),
    ("K2", "kotlin", "behaviour", "var steps = dates.count { it <= now }", "var steps = dates.count { it < now }", "boundary instant excluded"),
    ("K3", "kotlin", "behaviour", "- if (now < today) 1 else 0)", "- if (now <= today) 1 else 0)", "tail boundary inclusive"),
    ("K4", "kotlin", "behaviour", "val elapsedSeconds = (nowMillis - anchorMillis) / 1000L", "val elapsedSeconds = (nowMillis - anchorMillis + 999L) / 1000L", "rounds elapsed up"),
    ("K5", "kotlin", "behaviour", "if (initialQuoteOnly && steps > 0) return 1 + ((steps - 1) % (poolSize - 1)).toInt()", "if (initialQuoteOnly && steps > 0) return ((steps - 1) % (poolSize - 1)).toInt()", "override slot reused"),
    ("K6", "kotlin", "behaviour", "java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone(\"UTC\"))", "java.util.Calendar.getInstance()", "civil day in local zone"),
    ("K7", "kotlin", "refactor", "currentPoolIndex", "poolIndexAt", "rename helper (no behaviour change)"),
    ("S1", "swift", "behaviour", "let published = scheduleDates.prefix { $0 <= date }.count", "let published = scheduleDates.prefix { $0 < date }.count", "boundary instant excluded"),
    ("S2", "swift", "behaviour", "base = elapsed <= 0 ? 0 : Int(elapsed / interval)", "base = elapsed <= 0 ? 0 : Int((elapsed / interval).rounded())", "rounds periods"),
    ("S3", "swift", "behaviour", "return published + max(0, days - (date < boundary ? 1 : 0))", "return published + max(0, days)", "tail ignores time of day"),
    ("S4", "swift", "behaviour", "return list[1 + ((base - 1) % (list.count - 1))]", "return list[base % list.count]", "override slot reused"),
    ("S5", "swift", "behaviour", "let list = authoritativePool ? self.pool : (eligible.isEmpty ? pool : eligible)", "let list = eligible.isEmpty ? pool : eligible", "ignores authoritative pool"),
    ("S6", "swift", "behaviour", "matchingPolicy: .nextTime", "matchingPolicy: .previousTimePreservingSmallerComponents", "timeline gap policy"),
    ("S7", "swift", "refactor", "let eligible = pool.filter { $0.tier <= maxTier }", "let eligible = pool.filter { maxTier >= $0.tier }", "operand order (no behaviour change)"),
]

def apply(text, old, new, mid):
    n = text.count(old)
    assert n >= 1, f"{mid}: pattern not found"
    return text.replace(old, new)

if __name__ == "__main__":
    cmd = sys.argv[2]
    if cmd == "list":
        for m in MUTANTS: print(m[0], m[1], m[2])
    elif cmd == "apply":   # mutate the contract-test tree
        mid, tree = sys.argv[3], sys.argv[4]
        m = next(x for x in MUTANTS if x[0] == mid)
        path = os.path.join(tree, KT if m[1] == "kotlin" else SW)
        text = open(path).read()
        open(path, "w").write(apply(text, m[3], m[4], mid))
    elif cmd == "harness":  # mutate the harness copy
        mid, d = sys.argv[3], sys.argv[4]
        m = next(x for x in MUTANTS if x[0] == mid)
        path = os.path.join(d, "Rotation.kt" if m[1] == "kotlin" else "Rotation.swift")
        text = open(path).read()
        open(path, "w").write(apply(text, m[3], m[4], mid))
    elif cmd == "json":
        print(json.dumps([dict(zip(["id", "platform", "kind", "old", "new", "description"], m)) for m in MUTANTS], indent=1))
