/**
 * Mutation check for lib/attempt-limit.ts.
 *
 * Breaks one guarantee at a time in the real file, runs the suite against the
 * broken copy, records which tests noticed, then puts the file back. Throwaway;
 * run with `bun run tmp/ratelimit/mutate.ts` from the repository root.
 */

import { spawnSync } from "node:child_process"
import { readFileSync, writeFileSync } from "node:fs"

const TARGET = "lib/attempt-limit.ts"
const SUITE = "lib/attempt-limit.test.ts"

type Mutation = { name: string; edits: [string, string][] }

const MUTATIONS: Mutation[] = [
  {
    name: "off by one on the count (blocks on the 11th, not the 10th)",
    edits: [
      [
        "return run !== undefined && run.failures >= maxAttempts",
        "return run !== undefined && run.failures > maxAttempts",
      ],
    ],
  },
  {
    name: "the window never expires (a run is never released)",
    edits: [
      [
        "    if (now - run.since >= windowMs) {\n      attempts.delete(key)\n      return undefined\n    }",
        "    if (false) {\n      attempts.delete(key)\n      return undefined\n    }",
      ],
    ],
  },
  {
    name: "the sweep never fires (expired entries are kept)",
    edits: [
      [
        "        if (now - run.since >= windowMs) attempts.delete(other)",
        "        if (false) attempts.delete(other)",
      ],
    ],
  },
  {
    name: "clearAttempts does nothing (a success does not reset)",
    edits: [
      [
        "    clearAttempts(key) {\n      attempts.delete(key)\n    },",
        "    clearAttempts(key) {\n      void key\n    },",
      ],
    ],
  },
  {
    name: "every username shares one bucket",
    edits: [
      [
        "  function live(key: string, now: number): Attempts | undefined {\n    const run = attempts.get(key)",
        '  function live(key: string, now: number): Attempts | undefined {\n    key = "shared"\n    const run = attempts.get(key)',
      ],
      [
        "      attempts.set(key, { failures: 1, since: now })",
        '      attempts.set("shared", { failures: 1, since: now })',
      ],
      [
        "    clearAttempts(key) {\n      attempts.delete(key)\n    },",
        '    clearAttempts(key) {\n      void key\n      attempts.delete("shared")\n    },',
      ],
    ],
  },
  {
    name: "retryAfter answers in milliseconds",
    edits: [
      [
        "      return Math.ceil((run.since + windowMs - now) / 1000)",
        "      return Math.ceil(run.since + windowMs - now)",
      ],
    ],
  },
  {
    name: "retryAfter rounds down (says try again now while still refusing)",
    edits: [
      [
        "      return Math.ceil((run.since + windowMs - now) / 1000)",
        "      return Math.floor((run.since + windowMs - now) / 1000)",
      ],
    ],
  },
  {
    name: "the cap is gone (the map grows without end)",
    edits: [
      [
        "      while (attempts.size >= maxKeys) {",
        "      while (false as boolean) {",
      ],
    ],
  },
]

const original = readFileSync(TARGET, "utf8")
const rows: string[] = []

try {
  for (const mutation of MUTATIONS) {
    let broken = original
    for (const [find, replace] of mutation.edits) {
      if (!broken.includes(find)) {
        throw new Error(`mutation "${mutation.name}" did not match: ${find}`)
      }
      broken = broken.replace(find, replace)
    }
    writeFileSync(TARGET, broken)

    const run = spawnSync("bun", ["test", SUITE], { encoding: "utf8" })
    const output = `${run.stdout}${run.stderr}`
    const failures = [...output.matchAll(/\(fail\) (.+?)(?: \[[\d.]+m?s\])?$/gm)]
      .map((match) => match[1].trim())

    rows.push(`\n### ${mutation.name}\ncaught by ${failures.length} test(s)`)
    for (const failure of failures) rows.push(`  - ${failure}`)
    if (failures.length === 0) rows.push("  !! NOT CAUGHT")
  }
} finally {
  writeFileSync(TARGET, original)
}

console.log(rows.join("\n"))
