///|
pub fn assert_ok(result : Result[DecodedFrame, ProxyError]) -> DecodedFrame {
  match result {
    Ok(frame) => frame
    Err(error) =>
      abort("expected Ok, got error at \{error.offset}: \{error.context}")
  }
}

///|
pub fn assert_err_kind(
  result : Result[DecodedFrame, ProxyError],
  expected : ProxyErrorKind,
) -> Unit {
  match result {
    Err(error) =>
      if error.kind != expected {
        abort("unexpected proxy error kind")
      }
    Ok(_) => abort("expected proxy error")
  }
}

///|
pub fn assert_bytes_eq(actual : Bytes, expected : Bytes) -> Unit {
  if !bytes_equal_constant_time(actual, expected) {
    abort(
      "byte sequences differ: \{hex_encode(actual)} != \{hex_encode(expected)}",
    )
  }
}

///|
fn raw_tlv_eq(left : RawTlv, right : RawTlv) -> Bool {
  left.type_code == right.type_code &&
  bytes_equal_constant_time(left.value, right.value)
}

///|
pub fn header_semantic_eq(left : ProxyHeader, right : ProxyHeader) -> Bool {
  if left.version != right.version ||
    left.command != right.command ||
    left.family != right.family ||
    left.transport != right.transport ||
    left.tlvs.length() != right.tlvs.length() {
    return false
  }
  for index = 0; index < left.tlvs.length(); index = index + 1 {
    if !raw_tlv_eq(left.tlvs[index], right.tlvs[index]) {
      return false
    }
  }
  match (left.address, right.address) {
    (NoAddress, NoAddress) => true
    (RawUnsupported(a), RawUnsupported(b)) => bytes_equal_constant_time(a, b)
    (Ipv4(a), Ipv4(b)) =>
      bytes_equal_constant_time(
        Bytes::from_array(a.source_address.octets),
        Bytes::from_array(b.source_address.octets),
      ) &&
      bytes_equal_constant_time(
        Bytes::from_array(a.destination_address.octets),
        Bytes::from_array(b.destination_address.octets),
      ) &&
      a.source_port == b.source_port &&
      a.destination_port == b.destination_port
    (Ipv6(a), Ipv6(b)) =>
      bytes_equal_constant_time(
        Bytes::from_array(a.source_address.octets),
        Bytes::from_array(b.source_address.octets),
      ) &&
      bytes_equal_constant_time(
        Bytes::from_array(a.destination_address.octets),
        Bytes::from_array(b.destination_address.octets),
      ) &&
      a.source_port == b.source_port &&
      a.destination_port == b.destination_port
    (UnixAddress(a), UnixAddress(b)) =>
      bytes_equal_constant_time(a.source, b.source) &&
      bytes_equal_constant_time(a.destination, b.destination)
    _ => false
  }
}

///|
pub fn assert_header_eq(actual : ProxyHeader, expected : ProxyHeader) -> Unit {
  if !header_semantic_eq(actual, expected) {
    abort("proxy headers are not semantically equal")
  }
}

///|
pub fn assert_remaining_eq(frame : DecodedFrame, expected : Bytes) -> Unit {
  assert_bytes_eq(frame.remaining, expected)
}

///|
pub fn assert_roundtrip_v1(header : ProxyHeader) -> Unit {
  let encoded = encode_v1(header).unwrap()
  let decoded = decode_v1(encoded, DecodePolicy::v1_only()).unwrap()
  assert_header_eq(decoded.header, header)
  if decoded.remaining.length() != 0 {
    abort("v1 roundtrip produced payload")
  }
}

///|
pub fn assert_roundtrip_v2(header : ProxyHeader) -> Unit {
  let encoded = encode_v2(header).unwrap()
  let decoded = decode_v2(encoded, DecodePolicy::v2_only()).unwrap()
  assert_header_eq(decoded.header, header)
  if decoded.remaining.length() != 0 {
    abort("v2 roundtrip produced payload")
  }
}