///|
pub(all) struct DecisionExpectation {
  principal_id : String
  action : String
  resource_id : String
  expected : DecisionKind
} derive(Debug)

///|
pub(all) struct ParsedExpectations {
  expectations : Array[DecisionExpectation]
  diagnostics : Array[Diagnostic]
} derive(Debug)

///|
pub(all) struct ExpectationResult {
  expectation : DecisionExpectation
  actual : DecisionKind
  matched_requests : Int
  passed : Bool
} derive(Debug)

///|
pub(all) struct VerificationReport {
  policy_name : String
  passed : Int
  failed : Int
  inconclusive : Int
  results : Array[ExpectationResult]
  diagnostics : Array[Diagnostic]
} derive(Debug)

///|
fn parse_expected_decision(word : String) -> DecisionKind? {
  match word {
    "allow" => Some(Allowed)
    "deny" => Some(Denied)
    _ => None
  }
}

///|
/// Each line: expect PRINCIPAL ACTION RESOURCE allow|deny.
pub fn parse_expectations(source : String) -> ParsedExpectations {
  let expectations : Array[DecisionExpectation] = []
  let diagnostics : Array[Diagnostic] = []
  let lines = source.split("\n").collect()
  for index, raw in lines {
    let line = raw.to_owned().trim().to_owned()
    if line == "" || line.has_prefix("#") {
      continue
    }
    let parts = fields(line)
    if parts.length() != 5 || parts[0] != "expect" {
      diagnostics.push({
        code: "E001",
        severity: Error,
        message: "line \{index + 1}: expected expect PRINCIPAL ACTION RESOURCE allow|deny",
      })
      continue
    }
    match parse_expected_decision(parts[4]) {
      Some(expected) =>
        expectations.push({
          principal_id: parts[1],
          action: parts[2],
          resource_id: parts[3],
          expected,
        })
      None =>
        diagnostics.push({
          code: "E002",
          severity: Error,
          message: "line \{index + 1}: expected allow or deny",
        })
    }
  }
  if expectations.length() == 0 {
    diagnostics.push({
      code: "E003",
      severity: Error,
      message: "no valid expectations supplied",
    })
  }
  { expectations, diagnostics, }
}

///|
fn same_expectation_request(
  expectation : DecisionExpectation,
  request : AccessRequest,
) -> Bool {
  expectation.principal_id == request.principal_id &&
  expectation.action == request.action &&
  expectation.resource_id == request.resource_id
}

///|
/// A request match must be unique. Two sampled requests with different
/// attributes but the same three identifiers are ambiguous for this format.
pub fn verify_expectations(
  policy : AccessPolicy,
  universe : RequestUniverse,
  expectations : Array[DecisionExpectation],
) -> VerificationReport {
  let diagnostics : Array[Diagnostic] = []
  for issue in validate_policy(policy) {
    diagnostics.push(issue)
  }
  for issue in validate_universe(universe) {
    diagnostics.push(issue)
  }
  let results : Array[ExpectationResult] = []
  if has_errors(diagnostics) {
    return {
      policy_name: policy.name,
      passed: 0,
      failed: 0,
      inconclusive: expectations.length(),
      results,
      diagnostics,
    }
  }
  let mut passed = 0
  let mut failed = 0
  let mut inconclusive = 0
  for expectation in expectations {
    let mut count = 0
    let mut actual = Unknown
    for request in universe.requests {
      if same_expectation_request(expectation, request) {
        count += 1
        actual = evaluate_request(policy, universe, request).kind
      }
    }
    let success = count == 1 && actual == expectation.expected
    if count != 1 || actual is Unknown {
      inconclusive += 1
      diagnostics.push({
        code: "E004",
        severity: Error,
        message: "expectation for \{expectation.principal_id}/\{expectation.action}/\{expectation.resource_id} does not match exactly one evaluated request",
      })
    } else if success {
      passed += 1
    } else {
      failed += 1
    }
    results.push({
      expectation,
      actual,
      matched_requests: count,
      passed: success,
    })
  }
  {
    policy_name: policy.name,
    passed,
    failed,
    inconclusive,
    results,
    diagnostics,
  }
}

///|
pub fn render_verification(report : VerificationReport) -> String {
  let out = StringBuilder()
  out.write_string("Policy verification: \{report.policy_name}\n")
  out.write_string(
    "Passed: \{report.passed}, failed: \{report.failed}, inconclusive: \{report.inconclusive}\n",
  )
  for result in report.results {
    out.write_string(
      "\{result.expectation.principal_id} \{result.expectation.action} \{result.expectation.resource_id}: ",
    )
    if result.matched_requests != 1 {
      out.write_string("INCONCLUSIVE\n")
    } else if result.passed {
      out.write_string("PASS\n")
    } else {
      out.write_string("FAIL\n")
    }
  }
  for issue in report.diagnostics {
    out.write_string("\{issue.code}: \{issue.message}\n")
  }
  out.to_string()
}