///|
/// The field that prevented a request from matching a cassette entry.
pub(all) enum MismatchKind {
  NoCandidate
  Method
  Url
  Query
  Headers
  Body
  Sequence
} derive(Eq, @debug.Debug)

///|
/// One safe, structured difference for a candidate cassette entry.
pub(all) struct Mismatch {
  candidate_index : Int
  kind : MismatchKind
  expected : String
  actual : String
} derive(Eq, @debug.Debug)

///|
/// A deterministic mismatch report suitable for test output.
pub(all) struct MismatchReport {
  request_key : String
  differences : Array[Mismatch]
} derive(Eq, @debug.Debug)

///|
pub fn MismatchKind::label(self : MismatchKind) -> String {
  match self {
    NoCandidate => "no-candidate"
    Method => "method"
    Url => "url"
    Query => "query"
    Headers => "headers"
    Body => "body"
    Sequence => "sequence"
  }
}

///|
/// Render a stable human-readable report. Values are already reduced to safe
/// descriptors by the diagnostic builder.
pub fn MismatchReport::summary(self : MismatchReport) -> String {
  if self.differences.length() == 0 {
    return "no mismatch"
  }
  let lines : Array[String] = ["request mismatch: " + self.request_key]
  for difference in self.differences {
    lines.push(
      "candidate[" +
      difference.candidate_index.to_string() +
      "] " +
      difference.kind.label() +
      ": expected " +
      difference.expected +
      ", actual " +
      difference.actual,
    )
  }
  lines.join("\n")
}

///|
priv struct DiagnosticView {
  method : String
  base_url : String
  query : String
  headers : String
  body : String
  match_base_url : String
  match_query : String
  match_headers : String
  match_body : String
}

///|
fn safe_fingerprint(value : String) -> String {
  let mut result = 17
  for index in 0.. String {
  let without_fragment = match url.split_once("#") {
    Some((before, _)) => before.to_owned()
    None => url
  }
  match without_fragment.split_once("?") {
    None => ""
    Some((_, raw_query)) => {
      let parts : Array[String] = []
      for raw_view in raw_query.split("&") {
        let raw = raw_view.to_owned()
        if raw.length() == 0 {
          continue
        }
        match raw.split_once("=") {
          Some((key, value)) => {
            let name = key.to_owned().trim().to_owned().to_lower()
            if sensitive_query_parameter(key.to_owned(), config) {
              parts.push(name + "=")
            } else {
              parts.push(
                name +
                "=",
              )
            }
          }
          None => {
            let name = raw.trim().to_owned().to_lower()
            if sensitive_query_parameter(raw + "", config) {
              parts.push(name + "=")
            } else {
              parts.push(name + "=")
            }
          }
        }
      }
      parts.sort_by((left, right) => left.compare(right))
      parts.join("&")
    }
  }
}

///|
fn diagnostic_header_parts(
  headers : Array[Header],
  config : MatchConfig,
  redaction : RedactionConfig,
) -> (String, String) {
  let summary_parts : Array[String] = []
  let match_parts : Array[String] = []
  let safe_headers = redact_headers(headers, redaction)
  for header in safe_headers {
    if should_include_header(header.name + "", config) {
      let name = normalized_header_name(header.name + "")
      let normalized_value = normalized_header_value(header.value + "")
      match_parts.push(name + "=" + normalized_value)
      let value = if sensitive_header(header.name + "", redaction) {
        ""
      } else {
        ""
      }
      summary_parts.push(name + "=" + value)
    }
  }
  summary_parts.sort_by((left, right) => left.compare(right))
  match_parts.sort_by((left, right) => left.compare(right))
  (summary_parts.join("&"), match_parts.join("&"))
}

///|
fn diagnostic_body_parts(
  body : Body,
  config : MatchConfig,
  redaction : RedactionConfig,
) -> (String, String) {
  let safe_body = match redact_body(body, redaction) {
    Ok(value) => value
    Err(_) => return ("", "")
  }
  match config.body_mode {
    Ignore => ("", "")
    Exact =>
      match safe_body {
        Empty => ("empty", "empty")
        Text(value) => {
          let summary = "text"
          (summary, normalize_body(Text(value), Exact))
        }
        Base64(value) => {
          let summary = "base64"
          (summary, normalize_body(Base64(value), Exact))
        }
      }
  }
}

///|
fn diagnostic_view(
  request : Request,
  config : MatchConfig,
  redaction : RedactionConfig,
) -> DiagnosticView {
  let { method, url, headers, body, } = request
  let normalized_url = normalize_url(redact_url(url + "", redaction))
  let (match_base_url, match_query) = match normalized_url.split_once("?") {
    Some((base, query)) => (base.to_owned(), query.to_owned())
    None => (normalized_url + "", "")
  }
  let (header_summary, match_headers) = diagnostic_header_parts(
    headers, config, redaction,
  )
  let (body_summary, match_body) = diagnostic_body_parts(
    body, config, redaction,
  )
  {
    method: method.trim().to_owned().to_upper(),
    base_url: match_base_url + "",
    query: diagnostic_query(url, redaction),
    headers: header_summary,
    body: body_summary,
    match_base_url,
    match_query,
    match_headers,
    match_body,
  }
}

///|
fn diagnostic_key(view : DiagnosticView) -> String {
  "method=" +
  view.method +
  "\nurl=" +
  view.base_url +
  "\nquery=" +
  view.query +
  "\nheaders=" +
  view.headers +
  "\nbody=" +
  view.body
}

///|
fn add_difference(
  differences : Array[Mismatch],
  candidate_index : Int,
  kind : MismatchKind,
  expected : String,
  actual : String,
) -> Unit {
  differences.push({ candidate_index, kind, expected, actual, })
}

///|
/// Build a safe mismatch report while honoring consumed-entry sequence.
pub fn diagnose_mismatch_with_redaction(
  request : Request,
  cassette : Cassette,
  config : MatchConfig,
  consumed : Array[Bool],
  redaction : RedactionConfig,
) -> MismatchReport {
  let actual = diagnostic_view(request, config, redaction)
  let differences : Array[Mismatch] = []
  let mut has_unconsumed = false
  let mut exact_unconsumed = false
  let mut exact_consumed = false
  let mut exact_consumed_index = -1
  for index in 0.. MismatchReport {
  diagnose_mismatch_with_redaction(
    request,
    cassette,
    config,
    consumed,
    RedactionConfig::default(),
  )
}