// Differential pass-rate reporter (T-08 stats; see moon.pkg for the contract).
//
// Failure messages have the generated format
//   [] field= expected= got= input=
// and are grouped by their "..." signature (the message minus the
// leading "[] " and the trailing " input=") so that failure
// MODES can be counted, not just raw case counts.

///|
/// One domain row of the report.
struct DomainStats {
  name : String
  total : Int
  exempted : Int
  failures : Array[String]
  /// Acceptance gate in basis points of `total` (e.g. 9900 = 99.00 %).
  gate_bps : Int
}

///|
/// Extract the failure-mode signature from one generated failure message:
/// the part between the first " field=" and the trailing " input=" segment.
fn signature(msg : String) -> String {
  let start = match msg.find(" field=") {
    Some(i) => i + 1
    None => 0
  }
  let end = match msg.rev_find(" input=") {
    Some(i) => i
    None => msg.length()
  }
  if start < end {
    msg[start:end].to_owned()
  } else {
    msg
  }
}

///|
/// Parse a run of ASCII digits as a non-negative decimal integer.
fn parse_digits(s : String) -> Int? {
  let mut value = 0
  for ch in s {
    if ch >= '0' && ch <= '9' {
      value = value * 10 + (ch.to_int() - '0'.to_int())
    } else {
      return None
    }
  }
  Some(value)
}

///|
/// Extract the 0-based case index from the leading "[]" of a
/// generated failure message (`None` when the prefix is malformed).
fn case_index(msg : String) -> Int? {
  let open = msg.find("[")
  let close = msg.find("]")
  match (open, close) {
    (Some(o), Some(c)) if o < c => parse_digits(msg[o + 1:c].to_owned())
    _ => None
  }
}

///|
/// Print the per-mode group counts (with up to 3 example case indices each)
/// for one domain's failure list.
fn print_groups(name : String, failures : Array[String]) -> Unit {
  if failures.is_empty() {
    println("\{name} failure groups: (none)")
    return
  }
  let counts : Map[String, Int] = Map([])
  let examples : Map[String, Array[String]] = Map([])
  for msg in failures {
    let sig = signature(msg)
    counts[sig] = counts.get(sig).unwrap_or(0) + 1
    let idx = match case_index(msg) {
      Some(i) => "\{i}"
      None => "?"
    }
    match examples.get(sig) {
      Some(list) => if list.length() < 3 {
        list.push(idx)
      }
      None => examples[sig] = [idx]
    }
  }
  println("\{name} failure groups: \{counts.length()} distinct")
  for sig, count in counts {
    let ex = match examples.get(sig) {
      Some(list) => list.join(", ")
      None => ""
    }
    println("  n=\{count} e.g.case=[\{ex}] \{sig}")
  }
}

///|
/// Render `numerator/denominator` in percent with two decimals (integer
/// math in basis points — no float formatting drift).
fn pct_bp(numerator : Int, denominator : Int) -> String {
  if denominator <= 0 {
    return "n/a"
  }
  let bp = numerator * 10000 / denominator
  let whole = bp / 100
  let frac = bp % 100
  let frac_txt = if frac < 10 {
    "0\{frac}"
  } else {
    "\{frac}"
  }
  "\{whole}.\{frac_txt}%"
}

///|
/// Print one domain row and return whether the gate is met.
fn report(stats : DomainStats) -> Bool {
  let failed = stats.failures.length()
  let passed = stats.total - stats.exempted - failed
  let met = passed * 10000 >= stats.total * stats.gate_bps
  let gate_pct = pct_bp(stats.gate_bps, 10000)
  let status = if met {
    "MET"
  } else {
    "NOT-MET"
  }
  println(
    "\{stats.name}\ttotal=\{stats.total}\texempted=\{stats.exempted}\tpassed=\{passed}\tfailed=\{failed}\trate=\{pct_bp(passed, stats.total)}\tgate>=\{gate_pct}\t\{status}",
  )
  met
}

///|
fn main {
  let ua_failures = @differential.ua_failures()
  let os_failures = @differential.os_failures()
  let device_failures = @differential.device_failures()
  println("domain\ttotal\texempted\tpassed\tfailed\trate\tgate\tstatus")
  let rows = [
    {
      name: "browser",
      total: @differential.ua_cases.length(),
      exempted: @differential.ua_exempted_count,
      failures: ua_failures,
      gate_bps: 9900,
    },
    {
      name: "os",
      total: @differential.os_cases.length(),
      exempted: @differential.os_exempted_count,
      failures: os_failures,
      gate_bps: 9700,
    },
    {
      name: "device",
      total: @differential.device_cases.length(),
      exempted: @differential.device_exempted_count,
      failures: device_failures,
      gate_bps: 9700,
    },
  ]
  let mut all_met = true
  for row in rows {
    if !report(row) {
      all_met = false
    }
  }
  print_groups("browser", ua_failures)
  print_groups("os", os_failures)
  print_groups("device", device_failures)
  if all_met {
    println("GATES: ALL MET")
  } else {
    println("GATES: NOT ALL MET (see rows above)")
    // The CI "Differential gate report" step can only fail the build if this
    // process exits non-zero, so an unmet gate must abort (fail-loud, same
    // mechanism as src/ua_parser/rules/rules_init.mbt). The report above is
    // printed first so stdout carries the full table before the fatal exit.
    abort("GATES: NOT ALL MET (see rows above)")
  }
}