///|
fn resource(memory : Int64, cpu : Int64) -> @scheduler.Resources raise {
  @scheduler.Resources::new(memory_bytes=memory, cpu_millis=cpu)
}

///|
fn node(
  id : String,
  memory : Int64,
  cpu : Int64,
  parent? : String,
  gate~ : @scheduler.Gate = Open,
) -> @scheduler.Scope raise {
  @scheduler.Scope::new(id, resource(memory, cpu), parent?, gate~)
}

///|
fn request(
  scope : String,
  memory : Int64,
  cpu : Int64,
) -> @scheduler.Allocation raise {
  @scheduler.Allocation::new(scope, resource(memory, cpu))
}

///|
fn work(
  id : String,
  scope : String,
  memory : Int64,
  cpu : Int64,
  value~ : Int64 = 1L,
) -> @scheduler.Job raise {
  @scheduler.Job::new(id, value~, [request(scope, memory, cpu)])
}

///|
fn emit(
  name : String,
  scopes : Array[@scheduler.Scope],
  jobs : Array[@scheduler.Job],
) -> Unit raise {
  let ledger = @scheduler.Ledger::new(scopes)
  let plan = ledger.plan(jobs)
  let result : Json = {
    "kind": Json("selection"),
    "case": Json(name),
    "synthetic": Json(true),
    "scopes": Json(scopes.map(s => s.to_json())),
    "jobs": Json(jobs.map(j => j.to_json())),
    "plan": plan.to_json(),
  }
  println(result.stringify())
}

///|
fn main raise {
  let paired = [work("a", "left", 60L, 1L), work("b", "right", 60L, 1L)]
  emit(
    "shared-parent",
    [
      node("root", 100L, 100L),
      node("left", 100L, 100L, parent="root"),
      node("right", 100L, 100L, parent="root"),
    ],
    paired,
  )
  emit(
    "separate-parents",
    [
      node("one", 100L, 100L),
      node("two", 100L, 100L),
      node("left", 100L, 100L, parent="one"),
      node("right", 100L, 100L, parent="two"),
    ],
    paired,
  )
  let root = [node("root", 10L, 10L)]
  let a = work("a", "root", 10L, 10L, value=9L)
  let b = work("b", "root", 6L, 4L, value=6L)
  emit("complementary", root, [a, b, work("c", "root", 4L, 6L, value=6L)])
  emit("changed-demand", root, [a, b, work("c", "root", 7L, 6L, value=6L)])
  let gang = @scheduler.Job::new("gang", value=20L, [
    request("left", 4L, 4L),
    request("right", 4L, 4L),
  ])
  emit(
    "gang-one-branch-held",
    [
      node("root", 12L, 12L),
      node("left", 10L, 10L, parent="root"),
      node("right", 10L, 10L, parent="root", gate=Pressure),
    ],
    [gang, work("local", "left", 6L, 6L)],
  )
  emit(
    "gang-open",
    [
      node("root", 12L, 12L),
      node("left", 10L, 10L, parent="root"),
      node("right", 10L, 10L, parent="root"),
    ],
    [gang, work("local", "left", 6L, 6L)],
  )
  let ledger = @scheduler.Ledger::new([node("root", 100L, 100L)])
  let first = ledger.plan([work("a", "root", 60L, 60L)])
  let other = ledger.plan([work("b", "root", 60L, 60L)])
  let selected = first.commit()
  let stale_rejected = try {
    ignore(other.commit())
    false
  } catch {
    StalePlan => true
    _ => false
  }
  ledger.refresh([node("root", 50L, 50L)])
  let shrunk = ledger.balances().map(b => b.to_json())
  ledger.release("a")
  let restarted = ledger.plan([work("b", "root", 50L, 50L)]).commit()
  let lifecycle : Json = {
    "kind": Json("lifecycle"),
    "case": Json("commit-stale-shrink-release"),
    "synthetic": Json(true),
    "first_selected": Json(selected),
    "stale_rejected": Json(stale_rejected),
    "after_shrink": Json(shrunk),
    "after_release_selected": Json(restarted),
  }
  println(lifecycle.stringify())
  if @env.args().contains("--oracle") {
    // Deterministic input generation only. Python independently enumerates every subset.
    let mut seed = 1729L
    fn draw(bound : Int64) -> Int64 {
      seed = seed * 48271L % 2147483647L
      seed % bound
    }
    for k in 0..<60 {
      let gate : @scheduler.Gate = if k % 7 == 0 {
        Unknown
      } else if k % 5 == 0 {
        Pressure
      } else {
        Open
      }
      let scopes = [
        node("root", 8L + draw(13L), 8L + draw(13L)),
        node("left", 5L + draw(10L), 5L + draw(10L), parent="root"),
        node("right", 5L + draw(10L), 5L + draw(10L), parent="root", gate~),
      ]
      let jobs : Array[@scheduler.Job] = []
      let n = 2 + draw(7L).to_int()
      for i in 0..