///|
fn clean_about_blank() -> String {
  "{\"title\":\"Not Found\",\"status\":404}"
}

///|
fn registered_date_problem() -> String {
  "{\"type\":\"https://iana.org/assignments/http-problem-types#date\",\"title\":\"Date Not Acceptable\",\"status\":400}"
}

///|
fn batch_has_finding(
  report : ProblemBatchReport,
  kind : BatchFindingKind,
) -> Bool {
  for finding in report.findings() {
    if finding.kind() == kind {
      return true
    }
  }
  false
}

///|
fn batch_type_count(report : ProblemBatchReport, type_uri : String) -> Int {
  for frequency in report.type_frequencies() {
    if frequency.type_uri() == type_uri {
      return frequency.count()
    }
  }
  0
}

///|
fn batch_status_count(report : ProblemBatchReport, status : Int?) -> Int {
  for frequency in report.status_frequencies() {
    if frequency.status() == status {
      return frequency.count()
    }
  }
  0
}

///|
fn batch_extension_count(report : ProblemBatchReport, name : String) -> Int {
  for frequency in report.extension_frequencies() {
    if frequency.name() == name {
      return frequency.count()
    }
  }
  0
}

///|
test "empty batch is accepted" {
  let report = analyze_problem_batch([])
  assert_true(report.is_accepted())
  assert_int_eq(report.input_count(), 0)
  assert_int_eq(report.parsed_count(), 0)
  assert_int_eq(report.quality_score(), 100)
}

///|
test "clean about blank problem is accepted" {
  let report = analyze_problem_batch([clean_about_blank()])
  assert_true(report.decision() == Accept)
  assert_int_eq(report.parsed_count(), 1)
  assert_int_eq(report.about_blank_count(), 1)
  assert_int_eq(report.registered_type_count(), 1)
}

///|
test "registered explicit problem type is accepted" {
  let report = analyze_problem_batch([registered_date_problem()])
  assert_true(report.decision() == Accept)
  assert_int_eq(report.registered_type_count(), 1)
  assert_int_eq(report.unregistered_type_count(), 0)
}

///|
test "malformed document is a parse failure" {
  let report = analyze_problem_batch(["{"])
  assert_int_eq(report.parse_failure_count(), 1)
  assert_int_eq(report.parsed_count(), 0)
  assert_true(batch_has_finding(report, ParseFailure))
  assert_true(report.decision() == Reject)
}

///|
test "wrong type member becomes a diagnostic" {
  let report = analyze_problem_batch(["{\"title\":12,\"status\":400}"])
  assert_int_eq(report.diagnostic_count(), 1)
  assert_true(batch_has_finding(report, ParseDiagnostic))
  assert_true(report.decision() == Review)
}

///|
test "invalid status becomes a semantic violation" {
  let report = analyze_problem_batch(["{\"title\":\"Bad\",\"status\":700}"])
  assert_int_eq(report.semantic_issue_count(), 1)
  assert_true(batch_has_finding(report, SemanticViolation))
  assert_true(report.decision() == Reject)
}

///|
test "missing title and status are counted" {
  let report = analyze_problem_batch(["{}"])
  assert_int_eq(report.missing_title_count(), 1)
  assert_int_eq(report.missing_status_count(), 1)
  assert_int_eq(report.audit_issue_count(), 2)
  assert_true(report.decision() == Review)
}

///|
test "strict policy requires title" {
  let report = analyze_problem_batch(
    ["{\"status\":404}"],
    policy=ProblemBatchPolicy::strict(),
  )
  assert_true(batch_has_finding(report, MissingRequiredTitle))
  assert_true(report.decision() == Reject)
}

///|
test "strict policy requires status" {
  let report = analyze_problem_batch(
    ["{\"title\":\"Error\"}"],
    policy=ProblemBatchPolicy::strict(),
  )
  assert_true(batch_has_finding(report, MissingRequiredStatus))
  assert_true(report.decision() == Reject)
}

///|
test "relative type is recorded" {
  let report = analyze_problem_batch([
    "{\"type\":\"local/problem\",\"title\":\"Local\",\"status\":400}",
  ])
  assert_int_eq(report.relative_type_count(), 1)
  assert_true(batch_has_finding(report, RelativeProblemType))
  assert_true(report.decision() == Review)
}

///|
test "strict policy rejects relative type" {
  let report = analyze_problem_batch(
    ["{\"type\":\"local/problem\",\"title\":\"Local\",\"status\":400}"],
    policy=ProblemBatchPolicy::strict().with_max_audit_issues(10),
  )
  assert_true(report.decision() == Reject)
}

///|
test "custom policy can allow relative type" {
  let policy = ProblemBatchPolicy::strict()
    .with_allow_relative_types(true)
    .with_max_audit_issues(10)
  let report = analyze_problem_batch(
    ["{\"type\":\"local/problem\",\"title\":\"Local\",\"status\":400}"],
    policy~,
  )
  assert_true(report.decision() == Review)
}

///|
test "unknown absolute type is unregistered" {
  let report = analyze_problem_batch([
    "{\"type\":\"https://example.com/problems/quota\",\"title\":\"Quota\",\"status\":429}",
  ])
  assert_int_eq(report.unregistered_type_count(), 1)
  assert_true(batch_has_finding(report, UnregisteredProblemType))
}

///|
test "strict policy rejects unregistered type" {
  let report = analyze_problem_batch(
    [
      "{\"type\":\"https://example.com/problems/quota\",\"title\":\"Quota\",\"status\":429}",
    ],
    policy=ProblemBatchPolicy::strict(),
  )
  assert_true(report.decision() == Reject)
}

///|
test "custom unregistered type budget permits review" {
  let policy = ProblemBatchPolicy::strict().with_max_unregistered_types(1)
  let report = analyze_problem_batch(
    [
      "{\"type\":\"https://example.com/problems/quota\",\"title\":\"Quota\",\"status\":429}",
    ],
    policy~,
  )
  assert_true(report.decision() == Review)
}

///|
test "document limit is enforced" {
  let policy = ProblemBatchPolicy::default().with_max_documents(1)
  let report = analyze_problem_batch(
    [clean_about_blank(), clean_about_blank()],
    policy~,
  )
  assert_true(batch_has_finding(report, DocumentLimitExceeded))
  assert_true(report.decision() == Reject)
}

///|
test "negative policy budgets clamp to zero" {
  let policy = ProblemBatchPolicy::default()
    .with_max_documents(-1)
    .with_max_parse_failures(-2)
    .with_max_semantic_issues(-3)
    .with_max_audit_issues(-4)
    .with_max_unregistered_types(-5)
  assert_int_eq(policy.max_documents(), 0)
  assert_int_eq(policy.max_parse_failures(), 0)
  assert_int_eq(policy.max_semantic_issues(), 0)
  assert_int_eq(policy.max_audit_issues(), 0)
  assert_int_eq(policy.max_unregistered_types(), 0)
}

///|
test "policy boolean accessors reflect updates" {
  let policy = ProblemBatchPolicy::default()
    .with_require_title(true)
    .with_require_status(true)
    .with_allow_relative_types(false)
  assert_true(policy.requires_title())
  assert_true(policy.requires_status())
  assert_false(policy.allows_relative_types())
}

///|
test "permissive policy keeps parse failures reviewable" {
  let report = analyze_problem_batch(
    ["{"],
    policy=ProblemBatchPolicy::permissive(),
  )
  assert_true(report.decision() == Review)
  assert_int_eq(report.parse_failure_count(), 1)
}

///|
test "type frequencies aggregate effective types" {
  let report = analyze_problem_batch([
    clean_about_blank(),
    clean_about_blank(),
    registered_date_problem(),
  ])
  assert_int_eq(batch_type_count(report, "about:blank"), 2)
  assert_int_eq(
    batch_type_count(
      report, "https://iana.org/assignments/http-problem-types#date",
    ),
    1,
  )
}

///|
test "status frequencies aggregate declared status" {
  let report = analyze_problem_batch([
    clean_about_blank(),
    "{\"title\":\"Bad Request\",\"status\":400}",
    "{\"title\":\"No status\"}",
  ])
  assert_int_eq(batch_status_count(report, Some(404)), 1)
  assert_int_eq(batch_status_count(report, Some(400)), 1)
  assert_int_eq(batch_status_count(report, None), 1)
}

///|
test "extension frequencies aggregate names" {
  let report = analyze_problem_batch([
    "{\"title\":\"Bad Request\",\"status\":400,\"request_id\":\"a\"}", "{\"title\":\"Bad Request\",\"status\":400,\"request_id\":\"b\",\"errors\":[]}",
  ])
  assert_int_eq(batch_extension_count(report, "request_id"), 2)
  assert_int_eq(batch_extension_count(report, "errors"), 1)
}

///|
test "frequency arrays returned by report are copies" {
  let report = analyze_problem_batch([clean_about_blank()])
  let frequencies = report.type_frequencies()
  frequencies.clear()
  assert_int_eq(report.type_frequencies().length(), 1)
}

///|
test "finding array returned by report is a copy" {
  let report = analyze_problem_batch(["{}"])
  let findings = report.findings()
  findings.clear()
  assert_true(report.findings().length() > 0)
}

///|
test "member totals and maximum are calculated" {
  let report = analyze_problem_batch([
    clean_about_blank(),
    "{\"title\":\"Bad Request\",\"status\":400,\"request_id\":\"x\"}",
  ])
  assert_int_eq(report.total_member_count(), 5)
  assert_int_eq(report.max_member_count(), 3)
  assert_true(report.average_member_count() == 2.5)
}

///|
test "empty batch average member count is zero" {
  assert_true(analyze_problem_batch([]).average_member_count() == 0.0)
}

///|
test "informational status class is counted" {
  let report = analyze_problem_batch(["{\"title\":\"Continue\",\"status\":100}"])
  assert_int_eq(report.informational_status_count(), 1)
}

///|
test "successful status class is counted" {
  let report = analyze_problem_batch(["{\"title\":\"Created\",\"status\":201}"])
  assert_int_eq(report.successful_status_count(), 1)
}

///|
test "redirection status class is counted" {
  let report = analyze_problem_batch(["{\"title\":\"Redirect\",\"status\":302}"])
  assert_int_eq(report.redirection_status_count(), 1)
}

///|
test "client error status class is counted" {
  let report = analyze_problem_batch([clean_about_blank()])
  assert_int_eq(report.client_error_status_count(), 1)
}

///|
test "server error status class is counted" {
  let report = analyze_problem_batch([
    "{\"title\":\"Internal Server Error\",\"status\":500}",
  ])
  assert_int_eq(report.server_error_status_count(), 1)
}

///|
test "missing and invalid statuses use other class" {
  let report = analyze_problem_batch([
    "{\"title\":\"Missing\"}", "{\"title\":\"Invalid\",\"status\":700}",
  ])
  assert_int_eq(report.other_status_count(), 2)
}

///|
test "quality score remains perfect for clean input" {
  assert_int_eq(
    analyze_problem_batch([clean_about_blank()]).quality_score(),
    100,
  )
}

///|
test "quality score decreases for findings" {
  let report = analyze_problem_batch(["{", "{}"])
  assert_true(report.quality_score() < 100)
  assert_true(report.quality_score() >= 0)
}

///|
test "quality score clamps at zero" {
  let inputs : Array[String] = []
  for i = 0; i < 10; i = i + 1 {
    inputs.push("{")
  }
  assert_int_eq(
    analyze_problem_batch(inputs, policy=ProblemBatchPolicy::permissive()).quality_score(),
    0,
  )
}

///|
test "batch finding accessors expose stable evidence" {
  let finding = analyze_problem_batch(["{"]).findings()[0]
  assert_int_eq(finding.document_index(), 0)
  assert_true(finding.kind() == ParseFailure)
  assert_true(finding.context().contains("InvalidJson"))
}

///|
test "batch finding kind names are stable" {
  assert_str_eq(ParseFailure.name(), "ParseFailure")
  assert_str_eq(ParseDiagnostic.name(), "ParseDiagnostic")
  assert_str_eq(SemanticViolation.name(), "SemanticViolation")
  assert_str_eq(AuditWarning.name(), "AuditWarning")
}

///|
test "batch decision names are stable" {
  assert_str_eq(Accept.name(), "accept")
  assert_str_eq(Review.name(), "review")
  assert_str_eq(Reject.name(), "reject")
}

///|
test "summary json contains gate metrics" {
  let summary = analyze_problem_batch([clean_about_blank()]).summary_json()
  assert_true(summary.contains("\"decision\":\"accept\""))
  assert_true(summary.contains("\"quality_score\":100"))
  assert_true(summary.contains("\"input_count\":1"))
  assert_true(summary.contains("\"parsed_count\":1"))
}

///|
test "summary json reports malformed documents" {
  let summary = analyze_problem_batch(["{"]).summary_json()
  assert_true(summary.contains("\"decision\":\"reject\""))
  assert_true(summary.contains("\"parse_failures\":1"))
  assert_true(summary.contains("\"parsed_count\":0"))
}

///|
test "custom parser limits apply to every document" {
  let large = "{\"detail\":\"" + "x".repeat(20_000) + "\"}"
  let report = analyze_problem_batch(
    [clean_about_blank(), large],
    limits=Limits::strict(),
  )
  assert_int_eq(report.input_count(), 2)
  assert_int_eq(report.parsed_count(), 1)
  assert_int_eq(report.parse_failure_count(), 1)
}

///|
test "mixed batch preserves document indices" {
  let report = analyze_problem_batch([
    clean_about_blank(),
    "{",
    "{\"title\":\"Bad\",\"status\":700}",
  ])
  let mut saw_parse_at_one = false
  let mut saw_semantic_at_two = false
  for finding in report.findings() {
    if finding.kind() == ParseFailure && finding.document_index() == 1 {
      saw_parse_at_one = true
    }
    if finding.kind() == SemanticViolation && finding.document_index() == 2 {
      saw_semantic_at_two = true
    }
  }
  assert_true(saw_parse_at_one)
  assert_true(saw_semantic_at_two)
}