// fuzz.ce — fuzzer driver: generates random programs, runs differential, saves failures
//
// Usage:
//   cell fuzz                - run 100 iterations with a random seed
//   cell fuzz 500            - run 500 iterations with a random seed
//   cell fuzz --seed 42      - run 100 iterations starting at seed 42
//   cell fuzz 500 --seed 42  - run 500 iterations starting at seed 42
//
// Each iteration generates a random self-checking program, compiles it,
// runs it through both optimized and unoptimized paths, and compares results.
// Failures are saved to tests/fuzz_failures/ for reproduction.
var fd = use('fd')
var time = use('time')
var json = use('json')

var os_ref = use('os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn

var fuzzgen = use('fuzzgen')

var _args = args == null ? [] : args

// Parse arguments: fuzz [iterations] [--seed N]
var iterations = 100
var start_seed = null
var i = 0
var n = null
var run_err = null
var _run_one = null

while (i < length(_args)) {
  if (_args[i] == '--seed' && i + 1 < length(_args)) {
    start_seed = number(_args[i + 1])
    i = i + 2
  } else {
    n = number(_args[i])
    if (n != null && n > 0) iterations = n
    i = i + 1
  }
}

if (start_seed == null) {
  start_seed = floor(time.number() * 1000) % 1000000
}

if (!run_ast_noopt_fn) {
  log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
  $stop()
  return
}

// Ensure failures directory exists
var failures_dir = "tests/fuzz_failures"

function ensure_dir(path) {
  if (fd.is_dir(path)) return
  var parts = array(path, '/')
  var current = ''
  var j = 0
  while (j < length(parts)) {
    if (parts[j] != '') {
      current = current + parts[j] + '/'
      if (!fd.is_dir(current)) {
        fd.mkdir(current)
      }
    }
    j = j + 1
  }
}

// Deep comparison
function values_equal(a, b) {
  var j = 0
  if (a == b) return true
  if (is_null(a) && is_null(b)) return true
  if (is_null(a) || is_null(b)) return false
  if (is_array(a) && is_array(b)) {
    if (length(a) != length(b)) return false
    j = 0
    while (j < length(a)) {
      if (!values_equal(a[j], b[j])) return false
      j = j + 1
    }
    return true
  }
  return false
}

function describe(val) {
  if (is_null(val)) return "null"
  if (is_text(val)) return `"${val}"`
  if (is_number(val)) return text(val)
  if (is_logical(val)) return text(val)
  if (is_function(val)) return "<function>"
  return "<other>"
}

// Run a single fuzz iteration
function run_fuzz(seed_val) {
  var src = fuzzgen.generate(seed_val)
  var name = "fuzz_" + text(seed_val)
  var ast = null
  var mod_opt = null
  var mod_noopt = null
  var opt_err = null
  var noopt_err = null
  var errors = []
  var keys = null
  var k = 0
  var key = null
  var ret = null
  var _run = null
  var run_err = null
  var keys2 = null
  var k2 = 0
  var key2 = null
  var opt_result = null
  var noopt_result = null
  var opt_fn_err = null
  var noopt_fn_err = null
  var _run_opt = null
  var _run_noopt = null

  // Parse
  var _parse = function() {
    ast = analyze(src, name + ".cm")
  } disruption {
    push(errors, "parse error")
  }
  _parse()
  if (length(errors) > 0) return {seed: seed_val, errors: errors, src: src}

  // Run optimized
  var _opt = function() {
    mod_opt = run_ast_fn(name, ast, stone({use: function(p) { return use(p) }}))
  } disruption {
    opt_err = "disrupted"
  }
  _opt()

  // Run unoptimized
  var _noopt = function() {
    mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, stone({use: function(p) { return use(p) }}))
  } disruption {
    noopt_err = "disrupted"
  }
  _noopt()

  // Check module-level behavior
  if (opt_err != noopt_err) {
    push(errors, `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
    return {seed: seed_val, errors: errors, src: src}
  }
  if (opt_err != null) {
    // Both failed to load — consistent
    return {seed: seed_val, errors: errors, src: src}
  }

  // Run self-checks (optimized module)
  if (is_object(mod_opt)) {
    keys = array(mod_opt)
    k = 0
    while (k < length(keys)) {
      key = keys[k]
      if (is_function(mod_opt[key])) {
        ret = null
        run_err = null
        _run = function() {
          ret = mod_opt[key]()
        } disruption {
          run_err = "disrupted"
        }
        _run()

        if (is_text(ret)) {
          push(errors, `self-check ${key}: ${ret}`)
        }
        if (run_err != null) {
          push(errors, `self-check ${key}: unexpected disruption`)
        }
      }
      k = k + 1
    }
  }

  // Differential check on each function
  if (is_object(mod_opt) && is_object(mod_noopt)) {
    keys2 = array(mod_opt)
    k2 = 0
    while (k2 < length(keys2)) {
      key2 = keys2[k2]
      if (is_function(mod_opt[key2]) && is_function(mod_noopt[key2])) {
        opt_result = null
        noopt_result = null
        opt_fn_err = null
        noopt_fn_err = null

        _run_opt = function() {
          opt_result = mod_opt[key2]()
        } disruption {
          opt_fn_err = "disrupted"
        }
        _run_opt()

        _run_noopt = function() {
          noopt_result = mod_noopt[key2]()
        } disruption {
          noopt_fn_err = "disrupted"
        }
        _run_noopt()

        if (opt_fn_err != noopt_fn_err) {
          push(errors, `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`)
        } else if (!values_equal(opt_result, noopt_result)) {
          push(errors, `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
        }
      }
      k2 = k2 + 1
    }
  }

  return {seed: seed_val, errors: errors, src: src}
}

// Main loop
log.console(`Fuzzing: ${text(iterations)} iterations, starting seed=${text(start_seed)}`)
var total_pass = 0
var total_fail = 0
var result = null
var j = 0
var current_seed = 0
var fail_path = null

i = 0
while (i < iterations) {
  current_seed = start_seed + i
  run_err = null
  _run_one = function() {
    result = run_fuzz(current_seed)
  } disruption {
    run_err = "generator crashed"
  }
  _run_one()

  if (run_err != null) {
    result = {seed: current_seed, errors: [run_err], src: "// generator crashed"}
  }

  if (length(result.errors) > 0) {
    total_fail = total_fail + 1
    log.console(`  FAIL seed=${text(current_seed)}: ${result.errors[0]}`)

    // Save failure source for reproduction
    ensure_dir(failures_dir)
    fail_path = failures_dir + "/seed_" + text(current_seed) + ".cm"
    fd.slurpwrite(fail_path, stone(blob(result.src)))
    log.console(`    saved to ${fail_path}`)
  } else {
    total_pass = total_pass + 1
  }

  // Progress report every 100 iterations
  if ((i + 1) % 100 == 0) {
    log.console(`  progress: ${text(i + 1)}/${text(iterations)} (${text(total_pass)} passed, ${text(total_fail)} failed)`)
  }

  i = i + 1
}

log.console(`----------------------------------------`)
log.console(`Fuzz: ${text(total_pass)} passed, ${text(total_fail)} failed, ${text(iterations)} total`)
if (total_fail > 0) {
  log.console(`Failures saved to ${failures_dir}/`)
}

$stop()
