///|
fn tlv_grammar() -> @grammata.Grammar {
  let builder = @grammata.GrammarBuilder::new()
  let tag = builder.bytes([1, 2, 3], name="tag")
  let length = builder.bytes([0, 1, 2, 3, 4], name="length")
  let octet = builder.bytes([0, 1, 2, 127, 128, 254, 255], name="octet")
  let payload = builder.repeat(octet, minimum=0, maximum=4, name="payload")
  let frame = builder.sequence([tag, length, payload], name="frame")
  builder.set_start(frame).build().unwrap()
}

///|
fn tlv_target() -> @grammata.Target {
  @grammata.Target::new("tlv-demo", input => {
    if input.length() < 2 {
      return @grammata.rejected("truncated-header", features=[
        @grammata.feature("decoder-stage", "header"),
      ])
    }
    let tag = input[0].to_int()
    let declared = input[1].to_int()
    let actual = input.length() - 2
    let relation = if declared == actual { "matching" } else { "mismatching" }
    let features = [
      @grammata.feature("decoder-stage", "payload"),
      @grammata.feature("tag", tag.to_string()),
      @grammata.feature("length-relation", relation),
      @grammata.feature("payload-band", (actual / 2).to_string()),
    ]
    if declared != actual {
      @grammata.rejected("length-mismatch", features~)
    } else if tag == 3 &&
      actual > 0 &&
      input[input.length() - 1].to_int() == 255 {
      @grammata.failed("decoder", "reserved-tail-byte", features~)
    } else {
      @grammata.accepted(
        features~,
        result_digest=tag.to_string() + ":" + actual.to_string(),
      )
    }
  })
}

///|
fn run_tlv_demo(executions : Int) -> @grammata.CampaignResult {
  let config : @grammata.CampaignConfig = {
    ..@grammata.CampaignConfig::default(seed=7252L),
    executions,
    initial_inputs: executions.min(16),
    crossover_percent: 20,
  }
  @grammata.run_campaign(tlv_grammar(), tlv_target(), config).unwrap()
}

///|
fn main {
  let result = run_tlv_demo(1000)
  println(result.markdown_report(title="Binary TLV Fuzzing Demo"))
  if !result.failures.is_empty() {
    println("First replay artifact:")
    println(result.failures[0].encode_json())
  }
}