///|
/// Expected outcome for one upload contract conformance case.
pub(all) enum ConformanceExpectation {
  ExpectAccepted
  ExpectRejected
  ExpectParseError
} derive(Debug, Eq)

///|
/// Observed outcome while running one upload contract conformance case.
pub(all) enum ConformanceActual {
  ActualAccepted
  ActualRejected
  ActualParseError
} derive(Debug, Eq)

///|
/// One named multipart request and its expected contract behavior.
pub(all) struct UploadConformanceCase {
  name : String
  content_type : String
  body : String
  expectation : ConformanceExpectation
  required_issue_codes : Array[String]
  expected_risk : UploadRisk?
} derive(Debug, Eq)

///|
/// Detailed result for one conformance case.
pub(all) struct ConformanceCaseResult {
  name : String
  expectation : ConformanceExpectation
  actual : ConformanceActual
  passed : Bool
  issue_codes : Array[String]
  details : Array[String]
} derive(Debug, Eq)

///|
/// Aggregate result for a deterministic batch of upload requests.
pub(all) struct ConformanceSuiteResult {
  name : String
  cases : Array[ConformanceCaseResult]
} derive(Debug, Eq)

///|
/// Build a conformance case from a generated or captured request wrapper.
pub fn upload_conformance_case(
  name : String,
  request : MultipartRequest,
  expectation : ConformanceExpectation,
) -> UploadConformanceCase {
  {
    name,
    content_type: request.content_type,
    body: request.body,
    expectation,
    required_issue_codes: [],
    expected_risk: None,
  }
}

///|
/// Build a conformance case directly from HTTP Content-Type and body values.
pub fn raw_upload_conformance_case(
  name : String,
  content_type : String,
  body : String,
  expectation : ConformanceExpectation,
) -> UploadConformanceCase {
  {
    name,
    content_type,
    body,
    expectation,
    required_issue_codes: [],
    expected_risk: None,
  }
}

///|
/// Require a schema issue code to appear when this case is inspected.
pub fn UploadConformanceCase::require_issue_code(
  self : UploadConformanceCase,
  code : String,
) -> UploadConformanceCase {
  let required_issue_codes = conformance_clone_strings(
    self.required_issue_codes,
  )
  if !conformance_contains(required_issue_codes, code) {
    required_issue_codes.push(code)
  }
  {
    name: self.name,
    content_type: self.content_type,
    body: self.body,
    expectation: self.expectation,
    required_issue_codes,
    expected_risk: self.expected_risk,
  }
}

///|
/// Require the form analysis to produce a specific risk level.
pub fn UploadConformanceCase::with_expected_risk(
  self : UploadConformanceCase,
  risk : UploadRisk,
) -> UploadConformanceCase {
  {
    name: self.name,
    content_type: self.content_type,
    body: self.body,
    expectation: self.expectation,
    required_issue_codes: conformance_clone_strings(self.required_issue_codes),
    expected_risk: Some(risk),
  }
}

///|
/// Run a named batch of multipart requests against one upload contract.
pub fn run_conformance_suite(
  name : String,
  contract : UploadContract,
  cases : Array[UploadConformanceCase],
) -> ConformanceSuiteResult {
  let results = Array::new()
  let seen_names : Array[String] = []
  let mut i = 0
  while i < cases.length() {
    if conformance_contains(seen_names, cases[i].name) {
      results.push(duplicate_conformance_result(cases[i]))
    } else {
      seen_names.push(cases[i].name)
      results.push(run_conformance_case(contract, cases[i]))
    }
    i = i + 1
  }
  { name, cases: results }
}

///|
pub fn ConformanceExpectation::label(self : ConformanceExpectation) -> String {
  match self {
    ExpectAccepted => "accepted"
    ExpectRejected => "rejected"
    ExpectParseError => "parse-error"
  }
}

///|
pub fn ConformanceActual::label(self : ConformanceActual) -> String {
  match self {
    ActualAccepted => "accepted"
    ActualRejected => "rejected"
    ActualParseError => "parse-error"
  }
}

///|
pub fn ConformanceCaseResult::status_label(
  self : ConformanceCaseResult,
) -> String {
  if self.passed {
    "pass"
  } else {
    "fail"
  }
}

///|
pub fn ConformanceCaseResult::to_line(self : ConformanceCaseResult) -> String {
  let mut line = self.status_label() +
    "|" +
    self.name +
    "|expected=" +
    self.expectation.label() +
    "|actual=" +
    self.actual.label()
  if self.issue_codes.length() > 0 {
    line = line + "|issues=" + self.issue_codes.join(",")
  }
  if self.details.length() > 0 {
    line = line + "|details=" + self.details.join("; ")
  }
  line
}

///|
pub fn ConformanceSuiteResult::passed_count(
  self : ConformanceSuiteResult,
) -> Int {
  let mut count = 0
  let mut i = 0
  while i < self.cases.length() {
    if self.cases[i].passed {
      count = count + 1
    }
    i = i + 1
  }
  count
}

///|
pub fn ConformanceSuiteResult::failed_count(
  self : ConformanceSuiteResult,
) -> Int {
  self.cases.length() - self.passed_count()
}

///|
pub fn ConformanceSuiteResult::is_ok(self : ConformanceSuiteResult) -> Bool {
  self.failed_count() == 0
}

///|
pub fn ConformanceSuiteResult::summary(self : ConformanceSuiteResult) -> String {
  "conformance-suite=" +
  self.name +
  ", cases=" +
  self.cases.length().to_string() +
  ", passed=" +
  self.passed_count().to_string() +
  ", failed=" +
  self.failed_count().to_string()
}

///|
pub fn ConformanceSuiteResult::failed_cases(
  self : ConformanceSuiteResult,
) -> Array[ConformanceCaseResult] {
  let failed = Array::new()
  let mut i = 0
  while i < self.cases.length() {
    if !self.cases[i].passed {
      failed.push(self.cases[i])
    }
    i = i + 1
  }
  failed
}

///|
pub fn ConformanceSuiteResult::to_lines(
  self : ConformanceSuiteResult,
) -> Array[String] {
  let lines = [self.summary()]
  let mut i = 0
  while i < self.cases.length() {
    lines.push(self.cases[i].to_line())
    i = i + 1
  }
  lines
}

///|
/// Render a Markdown report suitable for CI artifacts and API review.
pub fn ConformanceSuiteResult::to_markdown(
  self : ConformanceSuiteResult,
) -> String {
  let lines = [
    "# Upload conformance report: " + conformance_markdown_cell(self.name),
    "",
    self.summary(),
    "",
    "| Status | Case | Expected | Actual | Issue codes | Details |",
    "| --- | --- | --- | --- | --- | --- |",
  ]
  let mut i = 0
  while i < self.cases.length() {
    let result = self.cases[i]
    lines.push(
      "| " +
      result.status_label() +
      " | " +
      conformance_markdown_cell(result.name) +
      " | " +
      result.expectation.label() +
      " | " +
      result.actual.label() +
      " | " +
      conformance_markdown_cell(result.issue_codes.join(", ")) +
      " | " +
      conformance_markdown_cell(result.details.join("; ")) +
      " |",
    )
    i = i + 1
  }
  lines.join("\n")
}

///|
fn run_conformance_case(
  contract : UploadContract,
  case : UploadConformanceCase,
) -> ConformanceCaseResult {
  match inspect_upload(case.content_type, case.body, contract) {
    Ok(inspection) => conformance_from_inspection(case, inspection)
    Err(err) => conformance_from_parse_error(case, err)
  }
}

///|
fn conformance_from_inspection(
  case : UploadConformanceCase,
  inspection : UploadInspection,
) -> ConformanceCaseResult {
  let actual = if inspection.is_accepted() {
    ActualAccepted
  } else {
    ActualRejected
  }
  let details = Array::new()
  if !conformance_outcome_matches(case.expectation, actual) {
    details.push(
      "expected " + case.expectation.label() + " but observed " + actual.label(),
    )
  }
  let issue_codes = conformance_issue_codes(inspection.validation)
  let mut i = 0
  while i < case.required_issue_codes.length() {
    if !conformance_contains(issue_codes, case.required_issue_codes[i]) {
      details.push("missing issue code: " + case.required_issue_codes[i])
    }
    i = i + 1
  }
  match case.expected_risk {
    Some(expected) =>
      if inspection.analysis.risk != expected {
        details.push(
          "expected risk " +
          expected.label() +
          " but observed " +
          inspection.analysis.risk.label(),
        )
      }
    None => ()
  }
  {
    name: case.name,
    expectation: case.expectation,
    actual,
    passed: details.length() == 0,
    issue_codes,
    details,
  }
}

///|
fn conformance_from_parse_error(
  case : UploadConformanceCase,
  err : MultipartError,
) -> ConformanceCaseResult {
  let details = Array::new()
  if case.expectation != ExpectParseError {
    details.push(
      "expected " +
      case.expectation.label() +
      " but observed parse-error: " +
      err.message(),
    )
  }
  if case.required_issue_codes.length() > 0 {
    details.push("schema issue codes are unavailable after a parse error")
  }
  if case.expected_risk is Some(_) {
    details.push("risk is unavailable after a parse error")
  }
  {
    name: case.name,
    expectation: case.expectation,
    actual: ActualParseError,
    passed: details.length() == 0,
    issue_codes: [],
    details,
  }
}

///|
fn duplicate_conformance_result(
  case : UploadConformanceCase,
) -> ConformanceCaseResult {
  {
    name: case.name,
    expectation: case.expectation,
    actual: ActualParseError,
    passed: false,
    issue_codes: [],
    details: ["duplicate conformance case name"],
  }
}

///|
fn conformance_outcome_matches(
  expected : ConformanceExpectation,
  actual : ConformanceActual,
) -> Bool {
  match (expected, actual) {
    (ExpectAccepted, ActualAccepted) => true
    (ExpectRejected, ActualRejected) => true
    (ExpectParseError, ActualParseError) => true
    _ => false
  }
}

///|
fn conformance_issue_codes(report : ValidationReport) -> Array[String] {
  let codes = Array::new()
  let mut i = 0
  while i < report.issues.length() {
    if !conformance_contains(codes, report.issues[i].code) {
      codes.push(report.issues[i].code)
    }
    i = i + 1
  }
  codes
}

///|
fn conformance_clone_strings(values : Array[String]) -> Array[String] {
  let copy = Array::new()
  let mut i = 0
  while i < values.length() {
    copy.push(values[i])
    i = i + 1
  }
  copy
}

///|
fn conformance_contains(values : Array[String], target : String) -> Bool {
  let mut i = 0
  while i < values.length() {
    if values[i] == target {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn conformance_markdown_cell(value : String) -> String {
  let out = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let code = value.code_unit_at(i)
    if code == 124 {
      out.write_string("\\|")
    } else if code == 10 || code == 13 {
      out.write_char(' ')
    } else {
      out.write_char(code.unsafe_to_char())
    }
    i = i + 1
  }
  out.to_string()
}