src/lightspeed/ops/protocol_contract_harness.gleam

//// Deterministic protocol contract verification and fuzz harness for M43.

import gleam/int
import gleam/list
import gleam/string
import lightspeed/agent/session
import lightspeed/diff
import lightspeed/pipeline/checkpoint
import lightspeed/protocol
import lightspeed/transport/contract
import lightspeed/transport/matrix

pub const snapshot_version = 1

/// M43 conformance scenarios.
pub type Scenario {
  ProtocolConformanceMatrixExpansion
  EnvelopeFuzzCorpusExpansion
  FuzzRegressionReplayCertification
  UnifiedProtocolEvidenceSurface
}

/// One M43 scenario outcome.
pub type ScenarioOutcome {
  ScenarioOutcome(
    scenario: Scenario,
    passed: Bool,
    deterministic: Bool,
    signature: String,
  )
}

/// Full M43 report.
pub type Report {
  Report(
    outcomes: List(ScenarioOutcome),
    failed_scenarios: Int,
    nondeterministic_failures: Int,
  )
}

type EnvelopeTrack {
  TransportEnvelope
  EventEnvelope
  DiffEnvelope
  PipelineEnvelope
}

type CorpusCase {
  CorpusCase(
    id: String,
    track: EnvelopeTrack,
    payload: String,
    expected: String,
  )
}

/// Run all M43 scenarios.
pub fn run_matrix() -> Report {
  let outcomes =
    [
      ProtocolConformanceMatrixExpansion,
      EnvelopeFuzzCorpusExpansion,
      FuzzRegressionReplayCertification,
      UnifiedProtocolEvidenceSurface,
    ]
    |> list.map(run_scenario)

  Report(
    outcomes: outcomes,
    failed_scenarios: count_failed(outcomes),
    nondeterministic_failures: count_nondeterministic(outcomes),
  )
}

/// Run one M43 scenario twice and require deterministic parity.
pub fn run_scenario(scenario: Scenario) -> ScenarioOutcome {
  let #(first_passed, first_signature) = evaluate(scenario)
  let #(second_passed, second_signature) = evaluate(scenario)
  let deterministic =
    first_passed == second_passed && first_signature == second_signature
  let passed = first_passed && second_passed && deterministic

  ScenarioOutcome(
    scenario: scenario,
    passed: passed,
    deterministic: deterministic,
    signature: first_signature,
  )
}

/// Scenario label.
pub fn scenario_label(scenario: Scenario) -> String {
  case scenario {
    ProtocolConformanceMatrixExpansion ->
      "protocol_conformance_matrix_expansion"
    EnvelopeFuzzCorpusExpansion -> "envelope_fuzz_corpus_expansion"
    FuzzRegressionReplayCertification -> "fuzz_regression_replay_certification"
    UnifiedProtocolEvidenceSurface -> "unified_protocol_evidence_surface"
  }
}

/// Stable pass/fail label.
pub fn pass_fail_label(outcome: ScenarioOutcome) -> String {
  case outcome.passed {
    True -> "pass"
    False -> "fail"
  }
}

/// Scenario signature accessor.
pub fn signature(outcome: ScenarioOutcome) -> String {
  outcome.signature
}

/// Scenario accessor.
pub fn scenario(outcome: ScenarioOutcome) -> Scenario {
  outcome.scenario
}

/// Determinism accessor.
pub fn deterministic(outcome: ScenarioOutcome) -> Bool {
  outcome.deterministic
}

/// Report outcomes accessor.
pub fn outcomes(report: Report) -> List(ScenarioOutcome) {
  report.outcomes
}

/// Failed scenario count.
pub fn failed_scenarios(report: Report) -> Int {
  report.failed_scenarios
}

/// Nondeterministic scenario count.
pub fn nondeterministic_failures(report: Report) -> Int {
  report.nondeterministic_failures
}

/// Stable report signature.
pub fn report_signature(report: Report) -> String {
  let entries =
    list.map(report.outcomes, fn(outcome) {
      scenario_label(outcome.scenario)
      <> "="
      <> pass_fail_label(outcome)
      <> ":deterministic="
      <> bool_label(outcome.deterministic)
      <> ":"
      <> outcome.signature
    })

  join_with(";", entries)
}

/// Deterministic snapshot signature for M43 fixture drift gates.
pub fn snapshot_signature() -> String {
  "m43.snapshot.v"
  <> int.to_string(snapshot_version)
  <> "|"
  <> report_signature(run_matrix())
}

/// Deterministic markdown report for M43 fixture scripts.
pub fn snapshot_report_markdown() -> String {
  let report = run_matrix()
  let failed = failed_scenarios(report)
  let nondeterministic = nondeterministic_failures(report)
  let status = case failed == 0 && nondeterministic == 0 {
    True -> "OK"
    False -> "FAIL"
  }

  "# Protocol Contract Fixture Report\n\n"
  <> "snapshot_version: "
  <> int.to_string(snapshot_version)
  <> "\n"
  <> "status: "
  <> status
  <> "\n"
  <> "failed_scenarios: "
  <> int.to_string(failed)
  <> "\n"
  <> "nondeterministic_failures: "
  <> int.to_string(nondeterministic)
  <> "\n\n"
  <> "snapshot_signature: "
  <> snapshot_signature()
  <> "\n\n"
  <> "report_signature: "
  <> report_signature(report)
  <> "\n"
}

fn evaluate(scenario: Scenario) -> #(Bool, String) {
  case scenario {
    ProtocolConformanceMatrixExpansion ->
      evaluate_protocol_conformance_matrix_expansion()
    EnvelopeFuzzCorpusExpansion -> evaluate_envelope_fuzz_corpus_expansion()
    FuzzRegressionReplayCertification ->
      evaluate_fuzz_regression_replay_certification()
    UnifiedProtocolEvidenceSurface ->
      evaluate_unified_protocol_evidence_surface()
  }
}

fn evaluate_protocol_conformance_matrix_expansion() -> #(Bool, String) {
  let #(mismatches, outcomes) =
    evaluate_protocol_cases(protocol_matrix_cases(), 1, 0, [])

  let passed =
    mismatches == 0
    && protocol.protocol_version == 1
    && diff.patch_stream_version == 1

  #(
    passed,
    "mismatches="
      <> int.to_string(mismatches)
      <> "|protocol_version="
      <> int.to_string(protocol.protocol_version)
      <> "|patch_stream_version="
      <> int.to_string(diff.patch_stream_version)
      <> "|cases="
      <> join_with(",", outcomes),
  )
}

fn evaluate_envelope_fuzz_corpus_expansion() -> #(Bool, String) {
  let transport_cases =
    cases_for_track(expanded_corpus(), TransportEnvelope, [])
  let event_cases = cases_for_track(expanded_corpus(), EventEnvelope, [])
  let diff_cases = cases_for_track(expanded_corpus(), DiffEnvelope, [])
  let pipeline_cases = cases_for_track(expanded_corpus(), PipelineEnvelope, [])

  let #(transport_mismatches, transport_outcomes) =
    evaluate_corpus_cases(transport_cases, 1, 0, [])
  let #(event_mismatches, event_outcomes) =
    evaluate_corpus_cases(event_cases, 1, 0, [])
  let #(diff_mismatches, diff_outcomes) =
    evaluate_corpus_cases(diff_cases, 1, 0, [])
  let #(pipeline_mismatches, pipeline_outcomes) =
    evaluate_corpus_cases(pipeline_cases, 1, 0, [])

  let total_mismatches =
    transport_mismatches
    + event_mismatches
    + diff_mismatches
    + pipeline_mismatches
  let all_outcomes =
    append_labels(
      append_labels(transport_outcomes, event_outcomes),
      append_labels(diff_outcomes, pipeline_outcomes),
    )

  let passed =
    total_mismatches == 0
    && list.length(transport_cases) == 5
    && list.length(event_cases) == 7
    && list.length(diff_cases) == 6
    && list.length(pipeline_cases) == 5

  #(
    passed,
    "mismatches="
      <> int.to_string(total_mismatches)
      <> "|transport="
      <> int.to_string(list.length(transport_cases))
      <> "/"
      <> int.to_string(transport_mismatches)
      <> "|event="
      <> int.to_string(list.length(event_cases))
      <> "/"
      <> int.to_string(event_mismatches)
      <> "|diff="
      <> int.to_string(list.length(diff_cases))
      <> "/"
      <> int.to_string(diff_mismatches)
      <> "|pipeline="
      <> int.to_string(list.length(pipeline_cases))
      <> "/"
      <> int.to_string(pipeline_mismatches)
      <> "|cases="
      <> join_with(",", all_outcomes),
  )
}

fn evaluate_fuzz_regression_replay_certification() -> #(Bool, String) {
  let regressions = error_cases(expanded_corpus(), [])
  let #(replay_mismatches, replay_outcomes) =
    replay_regression_cases(regressions, 1, 0, [])
  let passed =
    replay_mismatches == 0
    && list.length(regressions) >= 10
    && replay_outcomes != []

  #(
    passed,
    "replay_mismatches="
      <> int.to_string(replay_mismatches)
      <> "|regressions="
      <> int.to_string(list.length(regressions))
      <> "|replay="
      <> join_with(",", replay_outcomes),
  )
}

fn evaluate_unified_protocol_evidence_surface() -> #(Bool, String) {
  let #(protocol_passed, protocol_signature) =
    evaluate_protocol_conformance_matrix_expansion()
  let #(envelope_passed, envelope_signature) =
    evaluate_envelope_fuzz_corpus_expansion()
  let #(replay_passed, replay_signature) =
    evaluate_fuzz_regression_replay_certification()

  let drift_classes =
    drift_classes_from_cases(error_cases(expanded_corpus(), []), [])

  let passed =
    protocol_passed
    && envelope_passed
    && replay_passed
    && list.length(drift_classes) >= 6

  #(
    passed,
    "protocol="
      <> bool_label(protocol_passed)
      <> "|envelopes="
      <> bool_label(envelope_passed)
      <> "|replay="
      <> bool_label(replay_passed)
      <> "|drift_classes="
      <> join_with(",", drift_classes)
      <> "|protocol_signature="
      <> protocol_signature
      <> "|envelope_signature="
      <> envelope_signature
      <> "|replay_signature="
      <> replay_signature,
  )
}

fn protocol_matrix_cases() -> List(#(String, String)) {
  let escaped_event_payload =
    protocol.encode(protocol.Event(
      ref: "r|1",
      name: "save",
      payload: "{\"path\":\"a|b\"}",
    ))

  [
    #("hello|lightspeed|1", "ok:hello:lightspeed:1"),
    #("hello|lightspeed|0", "error:unsupported_version:0"),
    #("hello|lightspeed|2", "error:unsupported_version:2"),
    #("hello|lightspeed|abc", "error:invalid_version:abc"),
    #("hello|other|1", "error:unsupported_protocol:other"),
    #(escaped_event_payload, "ok:event:r|1:save"),
    #("ack|r-1", "ok:ack:r-1"),
    #("failure|f-1|fault", "ok:failure:f-1"),
    #("noop|x", "error:unknown_frame_tag:noop"),
  ]
}

fn expanded_corpus() -> List(CorpusCase) {
  [
    CorpusCase(
      id: "transport_hello_rejected",
      track: TransportEnvelope,
      payload: protocol.encode(protocol.hello()),
      expected: "error:unsupported_client_frame:hello",
    ),
    CorpusCase(
      id: "transport_diff_rejected",
      track: TransportEnvelope,
      payload: protocol.encode(protocol.Diff(ref: "d-1", html: "<p>x</p>")),
      expected: "error:unsupported_client_frame:diff",
    ),
    CorpusCase(
      id: "transport_decode_error",
      track: TransportEnvelope,
      payload: "event|e-1|increment|bad\\",
      expected: "error:protocol_decode_failed:invalid_escape_sequence",
    ),
    CorpusCase(
      id: "transport_payload_too_large",
      track: TransportEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "e-oversize",
        name: "increment",
        payload: "{\"blob\":\"1234567890123456789012345678901234567890\"}",
      )),
      expected: "error:unsupported_client_frame:payload_too_large",
    ),
    CorpusCase(
      id: "transport_unknown_event",
      track: TransportEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "e-unknown",
        name: "unknown",
        payload: "{}",
      )),
      expected: "error:unsupported_client_event:unknown",
    ),
    CorpusCase(
      id: "event_increment_ok",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "evt-1",
        name: "increment",
        payload: "{}",
      )),
      expected: "ok:event:increment",
    ),
    CorpusCase(
      id: "event_decrement_ok",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "evt-2",
        name: "decrement",
        payload: "{}",
      )),
      expected: "ok:event:decrement",
    ),
    CorpusCase(
      id: "event_unknown_name",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "evt-3",
        name: "unknown",
        payload: "{}",
      )),
      expected: "error:unsupported_event:unknown",
    ),
    CorpusCase(
      id: "event_bad_payload_shape",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "evt-4",
        name: "increment",
        payload: "not-json",
      )),
      expected: "error:malformed_event_payload",
    ),
    CorpusCase(
      id: "event_missing_ref",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Event(
        ref: "",
        name: "increment",
        payload: "{}",
      )),
      expected: "error:missing_event_ref",
    ),
    CorpusCase(
      id: "event_wrong_frame_kind",
      track: EventEnvelope,
      payload: protocol.encode(protocol.Ack(ref: "ack-1")),
      expected: "error:unsupported_event_envelope:ack",
    ),
    CorpusCase(
      id: "event_decode_error",
      track: EventEnvelope,
      payload: "event|evt-5|increment|bad\\",
      expected: "error:protocol_decode_failed:invalid_escape_sequence",
    ),
    CorpusCase(
      id: "diff_replace_ok",
      track: DiffEnvelope,
      payload: diff.encode_stream([
        diff.Replace(target: "#app", html: "<div>ok</div>"),
      ]),
      expected: "ok_ops:1:replace",
    ),
    CorpusCase(
      id: "diff_reorder_ok",
      track: DiffEnvelope,
      payload: diff.encode_stream([
        diff.ReorderKeyed(target: "#items", keys: ["k1", "k2"]),
      ]),
      expected: "ok_ops:1:reorder_keyed",
    ),
    CorpusCase(
      id: "diff_unsupported_version",
      track: DiffEnvelope,
      payload: "ps|2|0|0",
      expected: "error:unsupported_version:2",
    ),
    CorpusCase(
      id: "diff_missing_dictionary_entry",
      track: DiffEnvelope,
      payload: "ps|1|0|1|r,0,1",
      expected: "error:missing_dictionary_entry:0",
    ),
    CorpusCase(
      id: "diff_malformed_operation",
      track: DiffEnvelope,
      payload: "ps|1|1|#app|1|z,0",
      expected: "error:malformed_operation:z,0",
    ),
    CorpusCase(
      id: "diff_bad_dynamic_slots",
      track: DiffEnvelope,
      payload: "ps|1|3|#app|fp|slot|1|u,0,1,1,2",
      expected: "error:bad_field_count:dynamic_slots:2:1",
    ),
    CorpusCase(
      id: "pipeline_checkpoint_ok",
      track: PipelineEnvelope,
      payload: "checkpoint|run-1|extract_orders|1|10|100|source-a|101",
      expected: "ok:run=run-1|stage=extract_orders|sequence=1",
    ),
    CorpusCase(
      id: "pipeline_bad_field_count",
      track: PipelineEnvelope,
      payload: "checkpoint|run-1|extract_orders|1|10|100|source-a",
      expected: "error:bad_field_count:pipeline_checkpoint:8:7",
    ),
    CorpusCase(
      id: "pipeline_invalid_integer",
      track: PipelineEnvelope,
      payload: "checkpoint|run-1|extract_orders|a|10|100|source-a|101",
      expected: "error:invalid_integer:sequence:a",
    ),
    CorpusCase(
      id: "pipeline_invalid_checkpoint",
      track: PipelineEnvelope,
      payload: "checkpoint|run-1|extract_orders|1|-1|100|source-a|101",
      expected: "error:invalid_checkpoint",
    ),
    CorpusCase(
      id: "pipeline_empty_stage",
      track: PipelineEnvelope,
      payload: "checkpoint|run-1||1|10|100|source-a|101",
      expected: "error:invalid_checkpoint",
    ),
  ]
}

fn evaluate_protocol_cases(
  cases: List(#(String, String)),
  index: Int,
  mismatches: Int,
  outcomes_rev: List(String),
) -> #(Int, List(String)) {
  case cases {
    [] -> #(mismatches, list.reverse(outcomes_rev))
    [entry, ..rest] -> {
      let #(payload, expected) = entry
      let actual = protocol_matrix_case_label(payload)
      let next_mismatches = case actual == expected {
        True -> mismatches
        False -> mismatches + 1
      }

      evaluate_protocol_cases(rest, index + 1, next_mismatches, [
        int.to_string(index) <> ":" <> actual,
        ..outcomes_rev
      ])
    }
  }
}

fn evaluate_corpus_cases(
  cases: List(CorpusCase),
  index: Int,
  mismatches: Int,
  outcomes_rev: List(String),
) -> #(Int, List(String)) {
  case cases {
    [] -> #(mismatches, list.reverse(outcomes_rev))
    [entry, ..rest] -> {
      let actual = corpus_case_label(entry)
      let next_mismatches = case actual == entry.expected {
        True -> mismatches
        False -> mismatches + 1
      }

      evaluate_corpus_cases(rest, index + 1, next_mismatches, [
        int.to_string(index) <> ":" <> entry.id <> ":" <> actual,
        ..outcomes_rev
      ])
    }
  }
}

fn replay_regression_cases(
  cases: List(CorpusCase),
  index: Int,
  mismatches: Int,
  outcomes_rev: List(String),
) -> #(Int, List(String)) {
  case cases {
    [] -> #(mismatches, list.reverse(outcomes_rev))
    [entry, ..rest] -> {
      let first = corpus_case_label(entry)
      let replayed = corpus_case_label(entry)
      let next_mismatches = case
        first == entry.expected
        && replayed == entry.expected
        && first == replayed
      {
        True -> mismatches
        False -> mismatches + 1
      }

      replay_regression_cases(rest, index + 1, next_mismatches, [
        int.to_string(index)
          <> ":"
          <> entry.id
          <> ":"
          <> first
          <> ":replay="
          <> replayed,
        ..outcomes_rev
      ])
    }
  }
}

fn protocol_matrix_case_label(payload: String) -> String {
  case protocol.decode(payload) {
    Ok(protocol.Hello(protocol_name, version)) ->
      "ok:hello:" <> protocol_name <> ":" <> int.to_string(version)
    Ok(protocol.Event(ref, name, _)) -> "ok:event:" <> ref <> ":" <> name
    Ok(protocol.Diff(ref, _)) -> "ok:diff:" <> ref
    Ok(protocol.Ack(ref)) -> "ok:ack:" <> ref
    Ok(protocol.Failure(ref, _)) -> "ok:failure:" <> ref
    Error(error) -> "error:" <> protocol.decode_error_to_string(error)
  }
}

fn corpus_case_label(entry: CorpusCase) -> String {
  case entry.track {
    TransportEnvelope -> transport_envelope_case_label(entry.payload)
    EventEnvelope -> event_envelope_case_label(entry.payload)
    DiffEnvelope -> diff_envelope_case_label(entry.payload)
    PipelineEnvelope -> pipeline_envelope_case_label(entry.payload)
  }
}

fn transport_envelope_case_label(payload: String) -> String {
  let payload_limit = case string.contains(payload, "e-oversize") {
    True -> 16
    False -> 64
  }

  case connect_transport(payload_limit) {
    Error(reason) -> "error:connect_failed:" <> reason
    Ok(state) -> {
      let result = matrix.receive(state, payload, 1, 1)
      case first_failure_reason(result.outbound_frames) {
        "" -> "ok:" <> frame_labels(result.outbound_frames)
        reason -> "error:" <> reason
      }
    }
  }
}

fn event_envelope_case_label(payload: String) -> String {
  case protocol.decode(payload) {
    Ok(protocol.Event(ref, name, event_payload)) ->
      case ref == "" {
        True -> "error:missing_event_ref"
        False ->
          case is_supported_event(name) {
            False -> "error:unsupported_event:" <> name
            True ->
              case is_structured_payload(event_payload) {
                True -> "ok:event:" <> name
                False -> "error:malformed_event_payload"
              }
          }
      }
    Ok(frame) -> "error:unsupported_event_envelope:" <> frame_tag(frame)
    Error(error) ->
      "error:protocol_decode_failed:" <> protocol.decode_error_to_string(error)
  }
}

fn diff_envelope_case_label(payload: String) -> String {
  case diff.decode_stream(payload) {
    Ok(patches) ->
      "ok_ops:"
      <> int.to_string(list.length(patches))
      <> ":"
      <> join_with(",", list.map(patches, diff.operation))
    Error(error) -> "error:" <> diff.decode_error_to_string(error)
  }
}

fn pipeline_envelope_case_label(payload: String) -> String {
  case string.split(payload, "|") {
    [
      "checkpoint",
      run_id,
      stage,
      sequence_text,
      offset_text,
      event_time_text,
      idempotency_key,
      at_ms_text,
    ] ->
      case parse_int_field("sequence", sequence_text) {
        Error(reason) -> reason
        Ok(sequence) ->
          case parse_int_field("offset", offset_text) {
            Error(reason) -> reason
            Ok(offset) ->
              case parse_int_field("event_time_ms", event_time_text) {
                Error(reason) -> reason
                Ok(event_time_ms) ->
                  case parse_int_field("at_ms", at_ms_text) {
                    Error(reason) -> reason
                    Ok(at_ms) -> {
                      let entry =
                        checkpoint.checkpoint(
                          run_id,
                          stage,
                          sequence,
                          checkpoint.watermark(offset, event_time_ms),
                          idempotency_key,
                          at_ms,
                        )
                      case checkpoint.valid(entry) {
                        True ->
                          "ok:run="
                          <> entry.run_id
                          <> "|stage="
                          <> entry.stage
                          <> "|sequence="
                          <> int.to_string(entry.sequence)
                        False -> "error:invalid_checkpoint"
                      }
                    }
                  }
              }
          }
      }
    tokens ->
      "error:bad_field_count:pipeline_checkpoint:8:"
      <> int.to_string(list.length(tokens))
  }
}

fn parse_int_field(field: String, value: String) -> Result(Int, String) {
  case int.parse(value) {
    Ok(parsed) -> Ok(parsed)
    Error(_) -> Error("error:invalid_integer:" <> field <> ":" <> value)
  }
}

fn connect_transport(
  max_payload_bytes: Int,
) -> Result(matrix.AdapterState, String) {
  case
    matrix.connect_with_policies(
      session.start("m43-transport", "proc-a", session.Rehydrate, 0, 100),
      matrix.ConnectRequest(
        route: "/counter",
        csrf_token: "csrf",
        origin: "https://example.test",
        now_ms: 0,
        prefer_fallback: False,
      ),
      matrix.WebSocketOnly,
      contract.allow_all("proc-a"),
      contract.allow_protection(),
      matrix.SecurityPolicy(
        require_https_origin: True,
        require_csrf: True,
        max_payload_bytes: max_payload_bytes,
      ),
      matrix.TimeoutPolicy(
        heartbeat_timeout_ms: 30_000,
        reconnect_grace_ms: 60_000,
      ),
    )
  {
    matrix.Connected(state, _) -> Ok(state)
    matrix.Rejected(error) -> Error(contract.error_to_string(error))
  }
}

fn first_failure_reason(frames: List(String)) -> String {
  case frames {
    [] -> ""
    [frame, ..rest] ->
      case protocol.decode(frame) {
        Ok(protocol.Failure(_, reason)) -> reason
        _ -> first_failure_reason(rest)
      }
  }
}

fn frame_labels(frames: List(String)) -> String {
  join_with(
    ",",
    list.map(frames, fn(frame) {
      case protocol.decode(frame) {
        Ok(decoded) -> frame_tag(decoded)
        Error(error) ->
          "decode_error:" <> protocol.decode_error_to_string(error)
      }
    }),
  )
}

fn frame_tag(frame: protocol.Frame) -> String {
  case frame {
    protocol.Hello(_, _) -> "hello"
    protocol.Event(_, _, _) -> "event"
    protocol.Diff(_, _) -> "diff"
    protocol.Ack(_) -> "ack"
    protocol.Failure(_, _) -> "failure"
  }
}

fn is_supported_event(name: String) -> Bool {
  name == "increment" || name == "decrement"
}

fn is_structured_payload(payload: String) -> Bool {
  string.starts_with(payload, "{") && string.ends_with(payload, "}")
}

fn cases_for_track(
  cases: List(CorpusCase),
  target: EnvelopeTrack,
  acc_rev: List(CorpusCase),
) -> List(CorpusCase) {
  case cases {
    [] -> list.reverse(acc_rev)
    [entry, ..rest] ->
      case entry.track == target {
        True -> cases_for_track(rest, target, [entry, ..acc_rev])
        False -> cases_for_track(rest, target, acc_rev)
      }
  }
}

fn error_cases(
  cases: List(CorpusCase),
  acc_rev: List(CorpusCase),
) -> List(CorpusCase) {
  case cases {
    [] -> list.reverse(acc_rev)
    [entry, ..rest] ->
      case string.starts_with(entry.expected, "error:") {
        True -> error_cases(rest, [entry, ..acc_rev])
        False -> error_cases(rest, acc_rev)
      }
  }
}

fn drift_classes_from_cases(
  cases: List(CorpusCase),
  classes_rev: List(String),
) -> List(String) {
  case cases {
    [] -> list.reverse(classes_rev)
    [entry, ..rest] -> {
      let next = drift_class(entry.expected)
      let next_classes = case contains(classes_rev, next) {
        True -> classes_rev
        False -> [next, ..classes_rev]
      }
      drift_classes_from_cases(rest, next_classes)
    }
  }
}

fn drift_class(expected: String) -> String {
  case string.split(expected, ":") {
    ["error", class, ..] -> class
    _ -> "unknown"
  }
}

fn append_labels(left: List(String), right: List(String)) -> List(String) {
  case left {
    [] -> right
    [value, ..rest] -> [value, ..append_labels(rest, right)]
  }
}

fn contains(values: List(String), target: String) -> Bool {
  case values {
    [] -> False
    [value, ..rest] ->
      case value == target {
        True -> True
        False -> contains(rest, target)
      }
  }
}

fn count_failed(outcomes: List(ScenarioOutcome)) -> Int {
  case outcomes {
    [] -> 0
    [outcome, ..rest] ->
      case outcome.passed {
        True -> count_failed(rest)
        False -> 1 + count_failed(rest)
      }
  }
}

fn count_nondeterministic(outcomes: List(ScenarioOutcome)) -> Int {
  case outcomes {
    [] -> 0
    [outcome, ..rest] ->
      case outcome.deterministic {
        True -> count_nondeterministic(rest)
        False -> 1 + count_nondeterministic(rest)
      }
  }
}

fn join_with(separator: String, values: List(String)) -> String {
  case values {
    [] -> ""
    [value] -> value
    [value, ..rest] -> value <> separator <> join_with(separator, rest)
  }
}

fn bool_label(value: Bool) -> String {
  case value {
    True -> "true"
    False -> "false"
  }
}