///|
/// Fuzz the three decoders: BER, v1/v2c messages, and v3 messages.
///
///   moon run e2e/fuzz
///
/// The property is narrow and absolute: **for any input at all, decoding
/// returns a value or an error, and never panics.** A panic is not an
/// error value — it escapes every `catch` and takes the process down — so
/// for a receiver, one malformed datagram from a stranger would be a
/// denial of service rather than a packet it ignores.
///
/// Random buffers alone are a weak test: they almost always die at the
/// first tag byte, never reaching the length arithmetic where the bugs
/// live. So three shapes are fed in, and the last two matter most:
///
///   1. random bytes, lengths 0..119;
///   2. every prefix of two valid messages, which walks the truncation
///      boundary one byte at a time;
///   3. every single-byte change of those messages, all 256 values at
///      every position.
///
/// Exits non-zero on a panic, which is what CI reads.
///
/// This found a real one: a four-octet length of `ff ff ff ff` reads as
/// -1 in a 32-bit Int, and the bound check that added it to the offset
/// wrapped, so a six-byte datagram reached the view slice with a negative
/// end. Six bytes. It is now rejected, and this suite is why it stays
/// rejected.

///|
/// A deterministic pseudo-random generator.
///
/// Deliberately weak — it only has to be reproducible and spread out. The
/// constants are small enough that the 32-bit multiply cannot overflow,
/// because an overflowing generator is a bug in the harness and would
/// hide the thing under test.
fn next(state : Array[Int]) -> Int {
  let v = (state[0] * 31 + 7) % 1000003
  state[0] = if v < 0 { v + 1000003 } else { v }
  state[0]
}

///|
fn random_bytes(state : Array[Int], len : Int) -> Bytes {
  let out : Array[Byte] = Array::make(len, (0).to_byte())
  for i in 0.. Int {
  let n = b.to_uint().reinterpret_as_int()
  if n >= 0x30 && n <= 0x39 {
    n - 0x30
  } else if n >= 0x61 && n <= 0x66 {
    n - 0x57
  } else {
    abort("not a hex digit")
  }
}

///|
fn unhex(text : String) -> Bytes {
  let src = @utf8.encode(text)
  let out : Array[Byte] = []
  let mut i = 0
  while i + 1 < src.length() {
    out.push(((nibble(src.at(i)) << 4) | nibble(src.at(i + 1))).to_byte())
    i = i + 2
  }
  Bytes::from_array(out)
}

///|
/// Decode `bytes` with all three decoders, swallowing their errors.
///
/// Errors are the expected outcome and are not interesting; a panic is,
/// and it cannot be caught here — it ends the process, which is the
/// signal this program exists to send.
fn try_all(bytes : Bytes) -> Unit {
  try {
    let _ = @ber.BerValue::decode(bytes)
  } catch {
    _ => ()
  }
  try {
    let _ = @pdu.Message::decode(bytes)
  } catch {
    _ => ()
  }
  try {
    let _ = @snmpv3.V3Message::decode(bytes)
  } catch {
    _ => ()
  }
}

///|
fn main {
  let state : Array[Int] = [123456789]
  let mut rounds = 0

  for _ in 0..<6000 {
    let len = next(state) % 120
    try_all(random_bytes(state, len))
    rounds = rounds + 1
  }

  // A v2c GetRequest for sysDescr.0, and the v3 discovery probe: both
  // are real messages, so their prefixes and mutations stay near the
  // shapes that matter.
  let v2c = unhex(
    "302902010104067075626c6963a01c020400000001020100020100300e300c06082b060102010101000500",
  )
  let v3 = unhex(
    "303e020103301102046bb9beba020300ffe30401040201030410300e0400020100020100040004000400301404000400a00e0204215080780201000201003000",
  )
  for whole in [v2c, v3] {
    let mut cut = 0
    while cut <= whole.length() {
      try_all(Bytes::from_array(whole.to_array()[:cut]))
      rounds = rounds + 1
      cut = cut + 1
    }
    let mut at = 0
    while at < whole.length() {
      let mut value = 0
      while value < 256 {
        let mutated = whole.to_array()
        mutated[at] = value.to_byte()
        try_all(Bytes::from_array(mutated))
        rounds = rounds + 1
        value = value + 1
      }
      at = at + 1
    }
  }

  // Text, for the OID parser. Two dots in a row and a stray letter are
  // the shapes a hand-edited config produces.
  let alphabet = "0123456789..x"
  for _ in 0..<3000 {
    let len = next(state) % 24
    let mut text = ""
    for _ in 0.. ()
    }
    rounds = rounds + 1
  }

  // The decryption path. A wrong key, or a corrupted ciphertext, gives
  // arbitrary bytes that are then parsed as a scoped PDU, so the shapes
  // below are the ones a real receiver meets rather than a hypothetical.
  let engine : @snmpv3.Engine = {
    id: unhex("80001f8880a32a253f0921cb6a00000000"),
    boots: 1,
    time: 100,
  }
  let security : @snmpv3.Security = {
    level: @snmpv3.AuthPriv,
    user: b"fuzz",
    auth_protocol: @snmpv3.Md5,
    auth_password: b"authpass",
    priv_protocol: @snmpv3.Aes128,
    priv_password: b"privpass",
  }
  let keys = security.localize(engine.id) catch {
    _ => abort("the fuzz keys would not localize")
  }
  let salt = unhex("0000000000000001")

  // `decrypt_aes` has to be total: any length of ciphertext, key held
  // fixed. Zero length and the lengths either side of a block boundary
  // are where its arithmetic would go wrong.
  let mut len = 0
  while len < 48 {
    let ciphertext = random_bytes(state, len)
    try {
      let _ = @snmpv3.decrypt_aes(ciphertext, keys.priv_key, engine, salt)
    } catch {
      _ => ()
    }
    rounds = rounds + 1
    len = len + 1
  }

  // `open` on random bytes: decode, verify, decrypt, parse — the whole
  // chain a receiver runs on every datagram.
  for _ in 0..<2000 {
    let len = next(state) % 96
    try_open(random_bytes(state, len), security, keys)
    rounds = rounds + 1
  }

  // And on mutations of a genuinely sealed message. These do not reach
  // the decryption — `open` verifies before it decrypts, and a changed
  // byte fails the authenticator — so the group below is the one that
  // covers that. What these do reach is the decoder and the TLV walk
  // that finds the authenticator, which is their point: a corrupted
  // envelope has to be refused, not crash on the way there.
  let envelope = seal_sample(engine, security, keys, salt) catch {
    _ => abort("the sample message would not seal")
  }
  let mut at = 0
  while at < envelope.length() {
    let mut value = 0
    while value < 256 {
      let mutated = envelope.to_array()
      mutated[at] = value.to_byte()
      try_open(Bytes::from_array(mutated), security, keys)
      rounds = rounds + 1
      value = value + 1
    }
    at = at + 5
  }

  // Random ciphertext that *passes* authentication, which is the only
  // way to reach the decryption: `open` verifies before it decrypts, so
  // a mutated envelope stops at the authenticator and never gets here.
  // Signing a message whose `data` is already encrypted does exactly
  // that — the authenticator covers the garbage, so it is accepted, and
  // the plaintext handed to the parser is arbitrary.
  let mut attempt = 0
  while attempt < 400 {
    let ciphertext = random_bytes(state, next(state) % 72)
    let message : @snmpv3.V3Message = {
      msg_id: attempt + 1,
      msg_max_size: @snmpv3.DEFAULT_MAX_SIZE,
      flags: @snmpv3.MsgFlags::of(@snmpv3.AuthPriv),
      security_model: @snmpv3.USM_SECURITY_MODEL,
      engine,
      user: b"fuzz",
      auth_params: b"",
      priv_params: salt,
      data: @snmpv3.Encrypted(ciphertext),
    }
    let wire = @snmpv3.seal(message, security, keys, salt) catch {
      _ => continue
    }
    try_open(wire, security, keys)
    rounds = rounds + 1
    attempt = attempt + 1
  }

  // Structurally valid BER values with the wrong shape: what decryption
  // hands the PDU decoder when the key is right and the sender is not.
  let shapes : Array[@ber.BerValue] = [
    @ber.BerValue::Tagged(0xA0, []),
    @ber.BerValue::Tagged(0xA0, [@ber.BerValue::Integer(1)]),
    @ber.BerValue::Tagged(0xA0, [
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Integer(0),
    ]),
    @ber.BerValue::Tagged(0xA0, [
      @ber.BerValue::OctetString(b"x"),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Sequence([]),
    ]),
    @ber.BerValue::Tagged(0xA0, [
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(999),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Sequence([]),
    ]),
    @ber.BerValue::Tagged(0xA4, []),
    @ber.BerValue::Tagged(0xA4, [
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(1),
    ]),
    @ber.BerValue::Tagged(0xA5, [@ber.BerValue::Integer(1)]),
    @ber.BerValue::Tagged(0xFF, []),
    @ber.BerValue::Tagged(0xA2, [
      @ber.BerValue::Integer(1),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Integer(0),
      @ber.BerValue::Sequence([
        @ber.BerValue::Sequence([@ber.BerValue::Integer(1)]),
      ]),
    ]),
    @ber.BerValue::Integer(1),
    @ber.BerValue::Null,
  ]
  for shape in shapes {
    try {
      let _ = @pdu.Pdu::from_ber(shape)
    } catch {
      _ => ()
    }
    rounds = rounds + 1
  }

  println("survived \{rounds} decoder rounds: no input panicked")
}

///|
/// `open` and swallow its error, for the fuzz loops.
fn try_open(
  bytes : Bytes,
  security : @snmpv3.Security,
  keys : @snmpv3.Keys,
) -> Unit {
  try {
    let _ = @snmpv3.open(bytes, security, keys)
  } catch {
    _ => ()
  }
}

///|
/// A sealed authPriv message with a few var-binds, so the ciphertext is
/// long enough that mutations land inside the encrypted region.
fn seal_sample(
  engine : @snmpv3.Engine,
  security : @snmpv3.Security,
  keys : @snmpv3.Keys,
  salt : Bytes,
) -> Bytes raise {
  let varbinds : Array[@pdu.VarBind] = [
    @pdu.VarBind::new(@oid.Oid::parse("1.3.6.1.2.1.1.1.0"), @ber.BerValue::Null),
    @pdu.VarBind::new(@oid.Oid::parse("1.3.6.1.2.1.1.5.0"), @ber.BerValue::Null),
  ]
  let message : @snmpv3.V3Message = {
    msg_id: 7,
    msg_max_size: @snmpv3.DEFAULT_MAX_SIZE,
    flags: @snmpv3.MsgFlags::of(@snmpv3.AuthPriv),
    security_model: @snmpv3.USM_SECURITY_MODEL,
    engine,
    user: b"fuzz",
    auth_params: b"",
    priv_params: b"",
    data: @snmpv3.Plain({
      context_engine_id: engine.id,
      context_name: b"",
      pdu: @pdu.Pdu::request(@pdu.GetRequest, 42, varbinds),
    }),
  }
  @snmpv3.seal(message, security, keys, salt)
}