///|
/// How a request corpus exercised one explicit rule.
pub(all) struct RuleCoverage {
  id : String
  selector_matches : Int
  condition_satisfied : Int
  condition_unsatisfied : Int
  condition_unknown : Int
  applied : Int
} derive(Debug, Eq)

///|
/// Aggregate authorization outcomes and per-rule exercise counts.
pub(all) struct PolicyCoverage {
  total_requests : Int
  allowed : Int
  explicit_denies : Int
  default_denies : Int
  rules : Array[RuleCoverage]
} derive(Debug, Eq)

///|
/// Return rule ids that no request matched at the selector level. This is a
/// coverage gap in the supplied corpus, not proof that a rule is unreachable.
pub fn PolicyCoverage::uncovered_rule_ids(
  self : PolicyCoverage,
) -> Array[String] {
  [
    for rule in self.rules if rule.selector_matches == 0 => rule.id
  ]
}

///|
/// Measure how a request corpus exercises the policy. Counts are per request:
/// duplicate requests intentionally count more than once. Rule order is kept.
pub fn Policy::coverage(
  self : Policy,
  requests : Array[Request],
) -> PolicyCoverage {
  let size = self.rules.length()
  let selected = Array::make(size, 0)
  let satisfied = Array::make(size, 0)
  let unsatisfied = Array::make(size, 0)
  let unknown = Array::make(size, 0)
  let applied = Array::make(size, 0)
  let mut allowed = 0
  let mut explicit_denies = 0
  let mut default_denies = 0
  for request in requests {
    let report = self.explain(request)
    match report.decision.reason {
      Allowed => allowed += 1
      ExplicitDeny => explicit_denies += 1
      NoMatchingRule => default_denies += 1
    }
    for index, rule in report.rules {
      match rule.condition_status {
        NotEvaluated => ()
        Satisfied => {
          selected[index] += 1
          satisfied[index] += 1
        }
        Unsatisfied => {
          selected[index] += 1
          unsatisfied[index] += 1
        }
        Unknown => {
          selected[index] += 1
          unknown[index] += 1
        }
      }
      if rule.applied {
        applied[index] += 1
      }
    }
  }
  let rules : Array[RuleCoverage] = []
  for index, rule in self.rules {
    rules.push({
      id: rule.id,
      selector_matches: selected[index],
      condition_satisfied: satisfied[index],
      condition_unsatisfied: unsatisfied[index],
      condition_unknown: unknown[index],
      applied: applied[index],
    })
  }
  {
    total_requests: requests.length(),
    allowed,
    explicit_denies,
    default_denies,
    rules,
  }
}

///|
fn RuleCoverage::to_json(self : RuleCoverage) -> Json {
  Json::object({
    "id": Json::string(self.id),
    "selector_matches": Json::number(Double::from_int(self.selector_matches)),
    "condition_satisfied": Json::number(
      Double::from_int(self.condition_satisfied),
    ),
    "condition_unsatisfied": Json::number(
      Double::from_int(self.condition_unsatisfied),
    ),
    "condition_unknown": Json::number(Double::from_int(self.condition_unknown)),
    "applied": Json::number(Double::from_int(self.applied)),
  })
}

///|
/// Serialize a policy coverage report as machine-readable JSON.
pub fn PolicyCoverage::to_json(self : PolicyCoverage) -> Json {
  Json::object({
    "total_requests": Json::number(Double::from_int(self.total_requests)),
    "allowed": Json::number(Double::from_int(self.allowed)),
    "explicit_denies": Json::number(Double::from_int(self.explicit_denies)),
    "default_denies": Json::number(Double::from_int(self.default_denies)),
    "uncovered_rule_ids": Json::array(
      [
        for id in self.uncovered_rule_ids() => Json::string(id)
      ],
    ),
    "rules": Json::array([ for rule in self.rules => rule.to_json() ]),
  })
}