///|
pub(all) struct RuleCoverage {
  rule_id : String
  matched_requests : Int
  decisive_requests : Int
} derive(Debug)

///|
pub(all) struct PolicyCoverage {
  policy_name : String
  evaluated_requests : Int
  allowed_requests : Int
  denied_requests : Int
  rule_coverage : Array[RuleCoverage]
  actions_seen : Array[String]
  principal_tenants_seen : Array[String]
  resource_tenants_seen : Array[String]
} derive(Debug)

///|
fn add_unique(values : Array[String], value : String) -> Unit {
  if !values.contains(value) {
    values.push(value)
  }
}

///|
fn principal_tenant(universe : RequestUniverse, id : String) -> String {
  for principal in universe.principals {
    if principal.id == id {
      return principal.tenant
    }
  }
  ""
}

///|
fn resource_tenant(universe : RequestUniverse, id : String) -> String {
  for resource in universe.resources {
    if resource.id == id {
      return resource.tenant
    }
  }
  ""
}

///|
/// Counts are observational: a rule with zero matches in this universe may
/// still match a request outside the supplied finite sample.
pub fn measure_policy_coverage(
  policy : AccessPolicy,
  universe : RequestUniverse,
) -> PolicyCoverage {
  let counts : Array[RuleCoverage] = []
  for rule in policy.rules {
    counts.push({ rule_id: rule.id, matched_requests: 0, decisive_requests: 0, })
  }
  let actions_seen : Array[String] = []
  let principal_tenants_seen : Array[String] = []
  let resource_tenants_seen : Array[String] = []
  let mut allowed_requests = 0
  let mut denied_requests = 0
  for request in universe.requests {
    add_unique(actions_seen, request.action)
    add_unique(
      principal_tenants_seen,
      principal_tenant(universe, request.principal_id),
    )
    add_unique(
      resource_tenants_seen,
      resource_tenant(universe, request.resource_id),
    )
    let decision = evaluate_request(policy, universe, request)
    match decision.kind {
      Allowed => allowed_requests += 1
      Denied => denied_requests += 1
      Unknown => ()
    }
    for index in 0.. Array[String] {
  let result : Array[String] = []
  for item in coverage.rule_coverage {
    if item.matched_requests == 0 {
      result.push(item.rule_id)
    }
  }
  result
}

///|
pub fn shadowed_rules(coverage : PolicyCoverage) -> Array[String] {
  let result : Array[String] = []
  for item in coverage.rule_coverage {
    if item.matched_requests > 0 && item.decisive_requests == 0 {
      result.push(item.rule_id)
    }
  }
  result
}

///|
pub fn render_coverage(coverage : PolicyCoverage) -> String {
  let out = StringBuilder()
  out.write_string("Policy: \{coverage.policy_name}\n")
  out.write_string("Explicit requests: \{coverage.evaluated_requests}\n")
  out.write_string("Allowed: \{coverage.allowed_requests}\n")
  out.write_string("Denied: \{coverage.denied_requests}\n")
  out.write_string("Distinct actions: \{coverage.actions_seen.length()}\n")
  out.write_string(
    "Principal tenants: \{coverage.principal_tenants_seen.length()}\n",
  )
  out.write_string(
    "Resource tenants: \{coverage.resource_tenants_seen.length()}\n",
  )
  for item in coverage.rule_coverage {
    out.write_string(
      "Rule \{item.rule_id}: matched \{item.matched_requests}, decisive \{item.decisive_requests}\n",
    )
  }
  out.to_string()
}