///|
/// The response body representation expected by an offline contract.
pub(all) enum ContractBodyKind {
  ExpectEmptyBody
  ExpectTextBody
  ExpectBase64Body
} derive(Eq, @debug.Debug)

///|
/// JSON value kinds supported by field-level response contracts.
pub(all) enum JsonValueKind {
  JsonNull
  JsonBoolean
  JsonNumber
  JsonString
  JsonArray
  JsonObject
} derive(Eq, @debug.Debug)

///|
/// One required JSON field and its expected value kind.
///
/// Paths use the same JSON Pointer or dot shorthand accepted by redaction.
pub(all) struct JsonFieldContract {
  path : String
  expected_kind : JsonValueKind
} derive(Eq, @debug.Debug)

///|
/// A rule for the direct elements of a JSON array field. The array itself is
/// located by `path`; every element must have `expected_kind`.
pub(all) struct JsonArrayElementContract {
  path : String
  expected_kind : JsonValueKind
} derive(Eq, @debug.Debug)

///|
/// One required response header value. The value is used only for an exact
/// comparison; reports expose a length and fingerprint instead of the value.
pub(all) struct HeaderValueContract {
  name : String
  expected_value : String
} derive(Eq, @debug.Debug)

///|
/// A small, transport-independent contract for a recorded or replayed response.
/// Empty arrays mean that the corresponding property is unrestricted.
pub(all) struct ResponseContract {
  allowed_statuses : Array[Int]
  required_headers : Array[String]
  required_header_values : Array[HeaderValueContract]
  body_kind : ContractBodyKind?
  json_fields : Array[JsonFieldContract]
  optional_json_fields : Array[JsonFieldContract]
  json_array_elements : Array[JsonArrayElementContract]
} derive(Eq, @debug.Debug)

///|
/// The category of one response contract violation.
pub(all) enum ContractViolationKind {
  StatusViolation
  MissingHeaderViolation
  HeaderValueViolation
  BodyKindViolation
  InvalidJsonViolation
  InvalidJsonPathViolation
  MissingJsonFieldViolation
  JsonTypeViolation
} derive(Eq, @debug.Debug)

///|
/// A safe response contract difference.
///
/// `expected` and `actual` only contain status or type descriptors. Response
/// header values, JSON values, and body text are never copied into this type.
pub(all) struct ContractViolation {
  kind : ContractViolationKind
  path : String
  expected : String
  actual : String
} derive(Eq, @debug.Debug)

///|
/// Stable contract validation output suitable for CI assertions.
pub(all) struct ContractReport {
  violations : Array[ContractViolation]
} derive(Eq, @debug.Debug)

///|
/// Create a contract that accepts every response.
pub fn ResponseContract::permissive() -> ResponseContract {
  {
    allowed_statuses: [],
    required_headers: [],
    required_header_values: [],
    body_kind: None,
    json_fields: [],
    optional_json_fields: [],
    json_array_elements: [],
  }
}

///|
pub fn ContractBodyKind::label(self : ContractBodyKind) -> String {
  match self {
    ExpectEmptyBody => "empty"
    ExpectTextBody => "text"
    ExpectBase64Body => "base64"
  }
}

///|
pub fn JsonValueKind::label(self : JsonValueKind) -> String {
  match self {
    JsonNull => "null"
    JsonBoolean => "boolean"
    JsonNumber => "number"
    JsonString => "string"
    JsonArray => "array"
    JsonObject => "object"
  }
}

///|
pub fn ContractViolationKind::label(self : ContractViolationKind) -> String {
  match self {
    StatusViolation => "status"
    MissingHeaderViolation => "missing-header"
    HeaderValueViolation => "header-value"
    BodyKindViolation => "body-kind"
    InvalidJsonViolation => "invalid-json"
    InvalidJsonPathViolation => "invalid-json-path"
    MissingJsonFieldViolation => "missing-json-field"
    JsonTypeViolation => "json-type"
  }
}

///|
pub fn ContractReport::is_valid(self : ContractReport) -> Bool {
  self.violations.length() == 0
}

///|
/// Render deterministic, value-safe output for CI and review.
pub fn ContractReport::summary(self : ContractReport) -> String {
  if self.violations.length() == 0 {
    return "contract valid"
  }
  let lines : Array[String] = [
    "contract violations: " + self.violations.length().to_string(),
  ]
  for violation in self.violations {
    lines.push(
      violation.kind.label() +
      " " +
      violation.path +
      ": expected " +
      violation.expected +
      ", actual " +
      violation.actual,
    )
  }
  lines.join("\n")
}

///|
fn response_body_kind(body : Body) -> ContractBodyKind {
  match body {
    Empty => ExpectEmptyBody
    Text(_) => ExpectTextBody
    Base64(_) => ExpectBase64Body
  }
}

///|
fn json_value_kind(value : Json) -> JsonValueKind {
  match value {
    Null => JsonNull
    True | False => JsonBoolean
    Number(_, ..) => JsonNumber
    String(_) => JsonString
    Array(_) => JsonArray
    Object(_) => JsonObject
  }
}

///|
fn status_is_allowed(status : Int, allowed : Array[Int]) -> Bool {
  if allowed.length() == 0 {
    return true
  }
  for candidate in allowed {
    if candidate == status {
      return true
    }
  }
  false
}

///|
fn allowed_statuses_label(statuses : Array[Int]) -> String {
  let parts : Array[String] = []
  for status in statuses {
    parts.push(status.to_string())
  }
  parts.join("|")
}

///|
fn response_has_header(headers : Array[Header], required : String) -> Bool {
  let expected = normalized_header_name(required)
  for header in headers {
    if normalized_header_name(header.name) == expected {
      return true
    }
  }
  false
}

///|
fn response_header_value(headers : Array[Header], required : String) -> String? {
  let expected = normalized_header_name(required)
  for header in headers {
    if normalized_header_name(header.name) == expected {
      return Some(header.value + "")
    }
  }
  None
}

///|
fn safe_header_value(value : String) -> String {
  ""
}

///|
fn json_value_at(value : Json, tokens : Array[String]) -> Result[Json?, Unit] {
  let mut current = value
  for token in tokens {
    current = match current {
      Object(members) =>
        match members.get(token + "") {
          Some(child) => child
          None => return Ok(None)
        }
      Array(items) =>
        match parse_array_index(token + "") {
          Some(index) =>
            match items.get(index) {
              Some(child) => child
              None => return Ok(None)
            }
          None => return Err(())
        }
      _ => return Ok(None)
    }
  }
  Ok(Some(current))
}

///|
fn add_contract_violation(
  violations : Array[ContractViolation],
  kind : ContractViolationKind,
  path : String,
  expected : String,
  actual : String,
) -> Unit {
  violations.push({ kind, path, expected, actual, })
}

///|
/// Validate a response without performing network or file-system access.
///
/// Violations are emitted in a stable order: status, required headers, header
/// values, body kind, required JSON fields, optional JSON fields, then array
/// element rules in caller-provided order.
pub fn Response::validate_contract(
  self : Response,
  contract : ResponseContract,
) -> ContractReport {
  let violations : Array[ContractViolation] = []
  if !status_is_allowed(self.status, contract.allowed_statuses) {
    add_contract_violation(
      violations,
      StatusViolation,
      "status",
      allowed_statuses_label(contract.allowed_statuses),
      self.status.to_string(),
    )
  }
  for required in contract.required_headers {
    if !response_has_header(self.headers, required) {
      add_contract_violation(
        violations,
        MissingHeaderViolation,
        "header:" + normalized_header_name(required),
        "present",
        "missing",
      )
    }
  }
  for required in contract.required_header_values {
    match response_header_value(self.headers, required.name) {
      None =>
        add_contract_violation(
          violations,
          HeaderValueViolation,
          "header:" + normalized_header_name(required.name),
          safe_header_value(required.expected_value),
          "missing",
        )
      Some(actual) =>
        if normalized_header_value(actual) !=
          normalized_header_value(required.expected_value) {
          add_contract_violation(
            violations,
            HeaderValueViolation,
            "header:" + normalized_header_name(required.name),
            safe_header_value(required.expected_value),
            safe_header_value(actual),
          )
        }
    }
  }
  match contract.body_kind {
    Some(expected) => {
      let actual = response_body_kind(self.body)
      if actual != expected {
        add_contract_violation(
          violations,
          BodyKindViolation,
          "body",
          expected.label(),
          actual.label(),
        )
      }
    }
    None => ()
  }
  if contract.json_fields.length() > 0 ||
    contract.optional_json_fields.length() > 0 ||
    contract.json_array_elements.length() > 0 {
    match self.body {
      Text(text) => {
        let parsed = @json.parse(text) catch {
          _ => {
            add_contract_violation(
              violations,
              InvalidJsonViolation,
              "body",
              "valid JSON text",
              "invalid JSON",
            )
            return { violations, }
          }
        }
        for field in contract.json_fields {
          let tokens = match parse_redaction_path(field.path) {
            Ok(tokens) => tokens
            Err(_) => {
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                field.path,
                "valid JSON Pointer or dot path",
                "invalid path",
              )
              continue
            }
          }
          match json_value_at(parsed, tokens) {
            Err(_) =>
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                field.path,
                "valid array index",
                "invalid array index",
              )
            Ok(None) =>
              add_contract_violation(
                violations,
                MissingJsonFieldViolation,
                field.path,
                field.expected_kind.label(),
                "missing",
              )
            Ok(Some(value)) => {
              let actual = json_value_kind(value)
              if actual != field.expected_kind {
                add_contract_violation(
                  violations,
                  JsonTypeViolation,
                  field.path,
                  field.expected_kind.label(),
                  actual.label(),
                )
              }
            }
          }
        }
        for field in contract.optional_json_fields {
          let tokens = match parse_redaction_path(field.path) {
            Ok(tokens) => tokens
            Err(_) => {
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                field.path,
                "valid JSON Pointer or dot path",
                "invalid path",
              )
              continue
            }
          }
          match json_value_at(parsed, tokens) {
            Err(_) =>
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                field.path,
                "valid array index",
                "invalid array index",
              )
            Ok(None) => ()
            Ok(Some(value)) => {
              let actual = json_value_kind(value)
              if actual != field.expected_kind {
                add_contract_violation(
                  violations,
                  JsonTypeViolation,
                  field.path,
                  field.expected_kind.label(),
                  actual.label(),
                )
              }
            }
          }
        }
        for rule in contract.json_array_elements {
          let tokens = match parse_redaction_path(rule.path) {
            Ok(tokens) => tokens
            Err(_) => {
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                rule.path,
                "valid JSON Pointer or dot path",
                "invalid path",
              )
              continue
            }
          }
          match json_value_at(parsed, tokens) {
            Err(_) =>
              add_contract_violation(
                violations,
                InvalidJsonPathViolation,
                rule.path,
                "valid array index",
                "invalid array index",
              )
            Ok(None) =>
              add_contract_violation(
                violations,
                MissingJsonFieldViolation,
                rule.path,
                "array",
                "missing",
              )
            Ok(Some(Array(items))) => {
              let mut index = 0
              for item in items {
                let actual = json_value_kind(item)
                if actual != rule.expected_kind {
                  add_contract_violation(
                    violations,
                    JsonTypeViolation,
                    rule.path + "/" + index.to_string(),
                    rule.expected_kind.label(),
                    actual.label(),
                  )
                }
                index += 1
              }
            }
            Ok(Some(other)) =>
              add_contract_violation(
                violations,
                JsonTypeViolation,
                rule.path,
                "array",
                json_value_kind(other).label(),
              )
          }
        }
      }
      other =>
        add_contract_violation(
          violations,
          InvalidJsonViolation,
          "body",
          "JSON text",
          response_body_kind(other).label(),
        )
    }
  }
  { violations, }
}