///|
/// An inventory-level mismatch between declared routes and observed responses.
pub struct CapabilityInventoryFinding {
  code : String
  name : String
  message : String
} derive(Eq, @debug.Debug)

///|
/// Audit of all observed route responses against named capability contracts.
pub struct CapabilityInventoryAudit {
  audits : Array[CapabilityResponseAudit]
  findings : Array[CapabilityInventoryFinding]
  ok : Bool
} derive(Eq, @debug.Debug)

///|
/// Check that every named contract has one observed response and every response
/// has one contract. Names are compared after trimming surrounding whitespace.
pub fn audit_capability_inventory(
  samples : Array[ResponseSample],
  contracts : Array[CapabilityContract],
) -> CapabilityInventoryAudit {
  let audits : Array[CapabilityResponseAudit] = []
  let findings : Array[CapabilityInventoryFinding] = []
  let contract_names : Array[String] = []
  let sample_names : Array[String] = []
  if samples.length() == 0 && contracts.length() == 0 {
    findings.push({
      code: "inventory-empty",
      name: "-",
      message: "no response samples or contracts were supplied",
    })
  }
  for contract in contracts {
    let name = trim(contract.name)
    if name == "" {
      findings.push({
        code: "empty-contract-name",
        name: "-",
        message: "a capability contract has no route name",
      })
      continue
    }
    if string_in(contract_names, name) {
      findings.push({
        code: "duplicate-contract-name",
        name,
        message: "more than one capability contract uses this route name",
      })
      continue
    }
    contract_names.push(name)
    let mut observed : ResponseSample? = None
    for sample in samples {
      if trim(sample.name) == name {
        observed = Some(sample)
        break
      }
    }
    match observed {
      Some(sample) => audits.push(audit_capability_response(sample, contract))
      None =>
        findings.push({
          code: "contract-without-sample",
          name,
          message: "declared route has no observed response sample",
        })
    }
  }
  for sample in samples {
    let name = trim(sample.name)
    if name == "" {
      findings.push({
        code: "empty-sample-name",
        name: "-",
        message: "an observed response sample has no route name",
      })
      continue
    }
    if string_in(sample_names, name) {
      findings.push({
        code: "duplicate-sample-name",
        name,
        message: "more than one response sample uses this route name",
      })
      continue
    }
    sample_names.push(name)
    if !string_in(contract_names, name) {
      findings.push({
        code: "sample-without-contract",
        name,
        message: "observed response has no capability contract",
      })
    }
  }
  let mut ok = findings.length() == 0
  for audit in audits {
    if !audit.report.ok {
      ok = false
    }
  }
  { audits, findings, ok, }
}

///|
/// Render the inventory and every route result for CI logs.
pub fn render_capability_inventory(audit : CapabilityInventoryAudit) -> String {
  let lines : Array[String] = [
    "permscope capability inventory",
    "audited=" +
    audit.audits.length().to_string() +
    " inventory_findings=" +
    audit.findings.length().to_string(),
  ]
  if audit.ok {
    lines.push("inventory: pass")
  } else {
    lines.push("inventory: fail")
  }
  for finding in audit.findings {
    lines.push(finding.code + " " + finding.name + " - " + finding.message)
  }
  for route_audit in audit.audits {
    lines.push(render_capability_response_audit(route_audit))
  }
  lines.join("\n")
}