///|
pub(all) struct MutationCase {
  id : String
  request_name : String
  field_path : String
  mutation_index : Int
  ordinal : Int
  payload : Bytes
  /// Group candidates combined into this case, outer first; empty for
  /// single-field cases. The primary field_path/mutation_index stay last.
  extra : Array[(String, Int)]
} derive(Debug, Eq)

///|
pub extend MutationCase with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend MutationCase with Eq::{not_equal, equal}

///|
pub(all) enum EnumerationState {
  Running
  Exhausted
  Limited
  Stopped
} derive(Debug, Eq)

///|
pub extend EnumerationState with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend EnumerationState with Eq::{not_equal, equal}

///|
/// One stack frame of the lazy case-sequence walker. Sequence frames track
/// the next child, plain frames the next mutation index, product frames the
/// current group pass.
pub struct StreamFrame {
  seq : CaseSeq
  mut child : Int
  mut pass : Int
  mut walking : Bool
}

///|
/// A pre-enumerated unit of the base case sequence for combinatorial
/// enumeration. `is_group` marks parts contributed by a product's group,
/// which upstream adds without consulting the skip set. `child` is the
/// availability-checked leaf entry.
pub struct StreamUnit {
  parts : Array[(Int, Int, Bool)]
  child : Int
}

///|
pub struct CombFrame {
  mut unit_index : Int
  /// Fixed incoming skip: availability of every sibling unit is checked
  /// against this snapshot.
  incoming : Map[Int, Bool]
  /// Accumulating skip: upstream's new_skip is created once per level and
  /// updated in place across sibling iterations, so later siblings descend
  /// with a skip set that contains earlier ones (session.py:1165-1175).
  acc : Map[Int, Bool]
  parts : Array[(Int, Int, Bool)]
}

///|
pub struct CaseStream {
  request : CompiledRequest
  limit : Int
  mut skip : Int
  mut ordinal : Int
  mut emitted : Int
  mut state : EnumerationState
  stack : Array[StreamFrame]
  combinatorial : Bool
  units : Array[StreamUnit]
  max_depth : Int?
  mut depth : Int
  mut depth_emitted : Int
  frames : Array[CombFrame]
  vars : Map[String, Bytes]?
}

///|
pub fn CompiledRequest::cases(
  self : CompiledRequest,
  start? : Int = 0,
  limit? : Int = 10000,
  vars? : Map[String, Bytes]? = None,
) -> CaseStream raise ModelError {
  guard start >= 0 && limit >= 0 else {
    raise Invalid("negative enumeration start/limit")
  }
  {
    request: self,
    limit,
    skip: start,
    ordinal: start,
    emitted: 0,
    state: Running,
    stack: [{ seq: self.seq, child: 0, pass: 0, walking: false, }],
    combinatorial: false,
    units: [],
    max_depth: None,
    depth: 0,
    depth_emitted: 0,
    frames: [],
    vars,
  }
}

///|
fn CompiledRequest::collect_units(
  self : CompiledRequest,
  seq : CaseSeq,
  prefix : Array[(Int, Int, Bool)],
  out : Array[StreamUnit],
) -> Unit {
  match seq {
    Plain(entry) => {
      let count = self.entries[entry].kind.count()
      for i in 0..
      for child in children {
        self.collect_units(child, prefix, out)
      }
    Product(group, inner) => {
      let passes = self.entries[group].kind.count()
      for gi in 0.. Bool {
  let names = parts.map(part => request.entries[part.0].path)
  for i in 0.. CaseStream raise ModelError {
  guard start >= 0 && limit >= 0 else {
    raise Invalid("negative enumeration start/limit")
  }
  {
    request: self,
    limit,
    skip: start,
    ordinal: start,
    emitted: 0,
    state: Running,
    stack: [{ seq: self.seq, child: 0, pass: 0, walking: false, }],
    combinatorial: false,
    units: [],
    max_depth: None,
    depth: 0,
    depth_emitted: 0,
    frames: [],
    vars,
  }
}

///|
pub fn CompiledRequest::combinatorial_cases_with_variables(
  self : CompiledRequest,
  vars : Map[String, Bytes]?,
  start? : Int = 0,
  limit? : Int = 10000,
  max_depth? : Int,
) -> CaseStream raise ModelError {
  guard start >= 0 && limit >= 0 else {
    raise Invalid("negative enumeration start/limit")
  }
  let units : Array[StreamUnit] = []
  self.collect_units(self.seq, [], units)
  {
    request: self,
    limit,
    skip: start,
    ordinal: start,
    emitted: 0,
    state: Running,
    stack: [],
    combinatorial: true,
    units,
    max_depth,
    depth: 1,
    depth_emitted: 0,
    frames: [{ unit_index: 0, incoming: Map([]), acc: Map([]), parts: [], }],
    vars,
  }
}

///|
/// Enumerate combinatorial cases: depth 1..max_depth (indefinite when
/// max_depth is absent, stopping at the first depth without a valid case),
/// combining that many units of the base sequence, deduplicated exactly like
/// upstream. Group products take part as whole units.
pub fn CompiledRequest::combinatorial_cases(
  self : CompiledRequest,
  start? : Int = 0,
  limit? : Int = 10000,
  max_depth? : Int,
) -> CaseStream raise ModelError {
  guard start >= 0 && limit >= 0 else {
    raise Invalid("negative enumeration start/limit")
  }
  let units : Array[StreamUnit] = []
  self.collect_units(self.seq, [], units)
  {
    request: self,
    limit,
    skip: start,
    ordinal: start,
    emitted: 0,
    state: Running,
    stack: [],
    combinatorial: true,
    units,
    max_depth,
    depth: 1,
    depth_emitted: 0,
    frames: [{ unit_index: 0, incoming: Map([]), acc: Map([]), parts: [], }],
    vars: None,
  }
}

///|
/// Advance the sequential tree walker to the next case position, returning
/// the mutated (entry, mutation index) pairs, outermost group first.
fn CaseStream::find(self : CaseStream) -> Array[(Int, Int)]? {
  while self.stack.length() > 0 {
    let frame = self.stack[self.stack.length() - 1]
    match frame.seq {
      Plain(entry) => {
        let count = self.request.entries[entry].kind.count()
        if frame.child < count {
          let mutation = frame.child
          frame.child += 1
          let parts : Array[(Int, Int)] = []
          for f in self.stack {
            if f.seq is Product(group, _) && f.walking {
              parts.push((group, f.pass))
            }
          }
          parts.push((entry, mutation))
          return Some(parts)
        }
        ignore(self.stack.pop())
      }
      Sequence(children) =>
        if frame.child < children.length() {
          let next = children[frame.child]
          frame.child += 1
          self.stack.push({ seq: next, child: 0, pass: 0, walking: false, })
        } else {
          ignore(self.stack.pop())
        }
      Product(_, inner) => {
        let group = match frame.seq {
          Product(group, _) => group
          _ => 0
        }
        let passes = self.request.entries[group].kind.count()
        if frame.walking {
          frame.walking = false
          frame.pass += 1
        } else if frame.pass < passes {
          frame.walking = true
          self.stack.push({ seq: inner, child: 0, pass: 0, walking: false, })
        } else {
          ignore(self.stack.pop())
        }
      }
    }
  }
  None
}

///|
/// Advance the combinatorial walker, returning the combined (entry, mutation
/// index) pairs of the next valid case, or None when enumeration ends.
fn CaseStream::comb_step(self : CaseStream) -> Array[(Int, Int)]? {
  while self.frames.length() > 0 {
    let frame = self.frames[self.frames.length() - 1]
    let mut descended = false
    while frame.unit_index < self.units.length() {
      let unit = self.units[frame.unit_index]
      frame.unit_index += 1
      if frame.incoming.contains(unit.child) {
        continue
      }
      if self.frames.length() < self.depth {
        let child_skip : Map[Int, Bool] = Map([])
        for key, _ in frame.acc {
          child_skip[key] = true
        }
        let parts = frame.parts.copy()
        for part in unit.parts {
          child_skip[part.0] = true
          parts.push(part)
        }
        // The accumulating set keeps growing for later siblings.
        for part in unit.parts {
          frame.acc[part.0] = true
        }
        self.frames.push({
          unit_index: 0,
          incoming: child_skip,
          acc: child_skip.copy(),
          parts,
        })
        descended = true
        break
      }
      let all = frame.parts.copy()
      for part in unit.parts {
        all.push(part)
      }
      if !contains_duplicate_paths(self.request, all) {
        self.depth_emitted += 1
        return Some(all.map(part => (part.0, part.1)))
      }
    }
    if descended {
      continue
    }
    ignore(self.frames.pop())
    if self.frames.is_empty() {
      // The depth finished; stop when it produced nothing or the maximum
      // depth is reached (session.py _generate_mutations_indefinitely).
      if self.depth_emitted == 0 {
        return None
      }
      match self.max_depth {
        Some(maximum) => if self.depth >= maximum { return None }
        None => ()
      }
      self.depth += 1
      self.depth_emitted = 0
      self.frames.push({
        unit_index: 0,
        incoming: Map([]),
        acc: Map([]),
        parts: [],
      })
    }
  }
  None
}

///|
fn CaseStream::build_case(
  self : CaseStream,
  parts : Array[(Int, Int)],
) -> MutationCase raise ModelError {
  let leaf = parts[parts.length() - 1]
  let kind = self.request.entries[leaf.0].kind
  if kind is Atom(field) {
    guard (field.candidate_length)(leaf.1) <= self.request.max_bytes.to_int64() else {
      raise Limit("field mutation exceeds request byte limit before allocation")
    }
  }
  let replacements : Array[(Int, Bytes)] = []
  let extra : Array[(String, Int)] = []
  for part in parts {
    let candidate = self.request.entries[part.0].kind.candidate(part.1)
    replacements.push((part.0, candidate))
    if part.0 != leaf.0 || part.1 != leaf.1 {
      extra.push((self.request.entries[part.0].path, part.1))
    }
  }
  // Upstream always emits a test case per candidate; blocks whose
  // dependencies disallow rendering contribute empty bytes
  // (boofuzz/blocks/block.py encode, 518c139).
  let payload = self.request.render_node(0, replacements, self.vars)
  let identity = parts
    .map(part => self.request.entries[part.0].path + ":\{part.1}")
    .join("+")
  {
    id: "v1:" + identity,
    request_name: self.request.name,
    field_path: self.request.entries[leaf.0].path,
    mutation_index: leaf.1,
    ordinal: self.ordinal,
    payload,
    extra,
  }
}

///|
pub fn CaseStream::next(self : CaseStream) -> MutationCase? raise ModelError {
  guard self.state == Running else { return None }
  errdefer {
    self.state = Stopped
  }
  while self.skip > 0 {
    let found = if self.combinatorial { self.comb_step() } else { self.find() }
    guard found is Some(_) else {
      self.state = Exhausted
      return None
    }
    self.skip -= 1
  }
  guard self.emitted < self.limit else {
    self.state = Limited
    return None
  }
  for ;; {
    guard self.ordinal < 2147483647 else {
      raise Limit("case ordinal overflow")
    }
    let found = if self.combinatorial { self.comb_step() } else { self.find() }
    let parts = match found {
      Some(parts) => parts
      None => {
        self.state = Exhausted
        return None
      }
    }
    let built = self.build_case(parts) catch {
      // An oversized candidate — the leaf alone or the total render above
      // the byte budget — cannot exist as a case; skip it and keep the
      // rest of the enumeration reachable instead of aborting the run.
      Limit(_) => {
        // Raw candidate positions must advance even when output is skipped.
        self.ordinal += 1
        continue
      }
      other => raise other
    }
    self.ordinal += 1
    self.emitted += 1
    return Some(built)
  }
}

///|
/// All mutated (path, mutation index) pairs of this case, outermost first.
pub fn MutationCase::parts(self : MutationCase) -> Array[(String, Int)] {
  let all = self.extra.copy()
  all.push((self.field_path, self.mutation_index))
  all
}

///|
pub fn CaseStream::stop(self : CaseStream) -> Unit {
  self.state = Stopped
}

///|
pub fn CaseStream::state(self : CaseStream) -> EnumerationState {
  self.state
}

///|
/// The next raw candidate position, including oversized skipped candidates.
pub fn CaseStream::position(self : CaseStream) -> Int {
  self.ordinal
}

///|
/// Cases not yet consumed from the start offset; nonzero after a stream
/// exhausts before reaching it.
pub fn CaseStream::remaining_skip(self : CaseStream) -> Int {
  self.skip
}

///|
fn CompiledRequest::seq_count(self : CompiledRequest, seq : CaseSeq) -> Int64 {
  match seq {
    Plain(entry) => self.entries[entry].kind.count().to_int64()
    Sequence(children) => {
      let mut total = 0L
      for child in children {
        total += self.seq_count(child)
      }
      total
    }
    Product(group, inner) =>
      self.entries[group].kind.count().to_int64() * self.seq_count(inner)
  }
}

///|
/// Case positions of the sequential stream, including positions inside group
/// product replays.
pub fn CompiledRequest::raw_mutation_count(self : CompiledRequest) -> Int64 {
  self.seq_count(self.seq)
}

///|
/// Number of sequential cases contributed by the entry at `path` (the
/// entry's own candidate count; group product replays are excluded).
pub fn CompiledRequest::count_at(
  self : CompiledRequest,
  path : String,
) -> Int raise ModelError {
  self.entries[self.resolve(path)].kind.count()
}

///|
/// Raw sequential case count emitted before the entry at `path` renders its
/// first plain case, walking the same tree the stream enumerates: unlike
/// count_at this includes group product replays of preceding grouped
/// blocks, so the result is a real stream position. Returns -1 when the
/// entry emits no plain cases at all (disabled or non-mutating entry).
pub fn CompiledRequest::raw_prefix_before(
  self : CompiledRequest,
  path : String,
) -> Int64 raise ModelError {
  let target = self.resolve(path)
  let found : Ref[Bool] = Ref(false)
  let prefix = self.raw_prefix_walk(self.seq, target, found)
  if found.val {
    prefix
  } else {
    -1L
  }
}

///|
fn CompiledRequest::raw_prefix_walk(
  self : CompiledRequest,
  seq : CaseSeq,
  target : Int,
  found : Ref[Bool],
) -> Int64 {
  if found.val {
    return 0L
  }
  match seq {
    Plain(entry) =>
      if entry == target {
        found.val = true
        0L
      } else {
        self.entries[entry].kind.count().to_int64()
      }
    Sequence(children) => {
      let mut total = 0L
      for child in children {
        total += self.raw_prefix_walk(child, target, found)
      }
      total
    }
    Product(group, inner) =>
      // The replay of the plain pass that ran as this node's preceding
      // sibling; if the target is inside that subtree, the sibling walk
      // already found it, so the replays only ever add their full count.
      self.entries[group].kind.count().to_int64() * self.seq_count(inner)
  }
}