///|
/// Stable structural diagnostics suitable for CLI and test output.
pub enum DiagnosticKind {
  Header
  Attribute
} derive(Eq, Debug)

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

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

///|
pub struct Diagnostic {
  kind : DiagnosticKind
  offset : Int
  message : String
}

///|
pub fn Diagnostic::new(
  kind : DiagnosticKind,
  offset : Int,
  message : String,
) -> Diagnostic {
  Diagnostic::{ kind, offset, message, }
}

///|
pub fn diagnose(bytes : Bytes) -> Array[Diagnostic] {
  let diagnostics = []
  if bytes.length() < 20 {
    diagnostics.push(
      Diagnostic::new(Header, 0, "RADIUS header requires 20 bytes"),
    )
    return diagnostics
  }
  if Code::from_int(bytes[0].to_int()) is Unknown(_) {
    diagnostics.push(Diagnostic::new(Header, 0, "unsupported RADIUS Code"))
  }
  let declared = (bytes[2].to_int() << 8) | bytes[3].to_int()
  if declared < 20 {
    diagnostics.push(
      Diagnostic::new(Header, 2, "declared length is below header size"),
    )
  } else if declared != bytes.length() {
    diagnostics.push(
      Diagnostic::new(Header, 2, "declared length does not match datagram"),
    )
  }
  if bytes.length() >= 20 {
    diagnose_attributes(bytes, diagnostics)
  }
  diagnostics
}

///|
fn diagnose_attributes(bytes : Bytes, diagnostics : Array[Diagnostic]) -> Unit {
  let mut offset = 20
  while offset < bytes.length() {
    if offset + 1 >= bytes.length() {
      diagnostics.push(
        Diagnostic::new(Attribute, offset, "attribute header is truncated"),
      )
      return
    }
    let length = bytes[offset + 1].to_int()
    if length < 2 {
      diagnostics.push(
        Diagnostic::new(Attribute, offset, "attribute length is below two"),
      )
      return
    }
    if offset + length > bytes.length() {
      diagnostics.push(
        Diagnostic::new(Attribute, offset, "attribute payload is truncated"),
      )
      return
    }
    offset = offset + length
  }
}

///|
/// Diagnose a packet while also checking the declared body boundary.
///
/// `diagnose` is intentionally non-throwing so command-line tools can show
/// every reliable problem in one pass. This helper adds a precise offset for
/// bytes that exist after the declared packet length.
pub fn diagnose_strict(bytes : Bytes) -> Array[Diagnostic] {
  let diagnostics = diagnose(bytes)
  if bytes.length() >= 4 {
    let declared = (bytes[2].to_int() << 8) | bytes[3].to_int()
    if declared >= 20 && declared < bytes.length() {
      diagnostics.push(
        Diagnostic::new(
          Header,
          declared,
          "datagram contains bytes after declared packet length",
        ),
      )
    }
  }
  diagnostics
}

///|
pub fn diagnostics_are_clean(diagnostics : Array[Diagnostic]) -> Bool {
  diagnostics.length() == 0
}

///|
pub fn Packet::diagnostics(self : Packet) -> Array[Diagnostic] {
  let errors = []
  for message in validate_packet(self) {
    errors.push(Diagnostic::new(Attribute, 20, message))
  }
  errors
}