///|
pub(all) struct ExpectedCase {
  call : @protocol.Call
  expected : @protocol.Value
}

///|
pub(all) struct Expectations {
  version : String
  provenance : String
  cases : Array[ExpectedCase]
}

///|
fn items(value : @ion.IonValue) -> Array[@ion.IonValue] raise {
  guard value.kind() is List(values) && !value.has_annotations() else {
    raise @protocol.Invalid("conformance.expected_list")
  }
  values
}

///|
pub fn Expectations::suite(self : Expectations) -> @protocol.Suite {
  { calls: self.cases.map(c => c.call), extensions: None, }
}

///|
pub fn Expectations::validate(self : Expectations) -> Unit raise {
  if self.version == "" ||
    self.version.length() > 128 ||
    self.provenance == "" ||
    self.provenance.length() > 4096 {
    raise @protocol.Invalid("conformance.missing_provenance")
  }
  self.suite().validate()
  for case in self.cases {
    // Reuse the bounded execution value validation for expected output values.
    @protocol.Suite::{
      calls: [
        {
          id: case.call.id,
          entry: case.call.entry,
          arguments: [case.expected],
        },
      ],
      extensions: None,
    }.validate()
  }
}

///|
pub fn Expectations::to_ion(self : Expectations) -> @ion.IonValue raise {
  self.validate()
  @protocol.record([
    ("profile", @ion.IonValue::string("morphir-conformance-v1")),
    ("version", @ion.IonValue::string(self.version)),
    ("provenance", @ion.IonValue::string(self.provenance)),
    (
      "cases",
      @ion.IonValue::list(
        self.cases.map(c => {
          @protocol.record([
            ("id", @ion.IonValue::string(c.call.id)),
            ("entry", @ion.IonValue::string(c.call.entry)),
            (
              "arguments",
              @ion.IonValue::list(c.call.arguments.map(v => v.to_ion())),
            ),
            ("expected", c.expected.to_ion()),
          ])
        }),
      ),
    ),
  ])
}

///|
pub fn Expectations::from_ion(value : @ion.IonValue) -> Expectations raise {
  decode_document(value, false)
}

///|
fn decode_document(value : @ion.IonValue, json : Bool) -> Expectations raise {
  fn semantic(v : @ion.IonValue) -> @protocol.Value raise {
    if json {
      if v.has_annotations() {
        raise @protocol.Invalid("conformance.annotation")
      }
      @protocol.Value::from_ion(v.with_annotation(@ion.symbol("morphir_value")))
    } else {
      @protocol.Value::from_ion(v)
    }
  }
  if value.has_annotations() {
    raise @protocol.Invalid("conformance.annotation")
  }
  let f = @protocol.fields(value, ["profile", "version", "provenance", "cases"])
  if @protocol.string(@protocol.required(f, "profile")) !=
    "morphir-conformance-v1" {
    raise @protocol.Invalid("conformance.profile")
  }
  let result = Expectations::{
    version: @protocol.string(@protocol.required(f, "version")),
    provenance: @protocol.string(@protocol.required(f, "provenance")),
    cases: items(@protocol.required(f, "cases")).map(value => {
      if value.has_annotations() {
        raise @protocol.Invalid("conformance.annotation")
      }
      let f = @protocol.fields(value, ["id", "entry", "arguments", "expected"])
      {
        call: {
          id: @protocol.string(@protocol.required(f, "id")),
          entry: @protocol.string(@protocol.required(f, "entry")),
          arguments: items(@protocol.required(f, "arguments")).map(semantic),
        },
        expected: semantic(@protocol.required(f, "expected")),
      }
    }),
  }
  result.validate()
  result
}

///|
pub fn Expectations::decode(
  bytes : Bytes,
  format~ : String = "ion-text",
) -> Expectations raise {
  if bytes.length() > 8388608 {
    raise @protocol.Invalid("conformance.input_limit")
  }
  if format == "json" {
    // Validate JSON syntax, then parse the original lexemes with Ion. The JSON
    // AST is deliberately unused so no semantic number passes through Double.
    ignore(@json.parse(@utf8.decode(bytes)))
    return decode_document(
      @text.read_ion(@utf8.decode(bytes)[:], fuel=Limited(@ion.Budget(128))),
      true,
    )
  }
  let value = match format {
    "ion-text" =>
      @text.read_ion(@utf8.decode(bytes)[:], fuel=Limited(@ion.Budget(128)))
    "ion-binary" =>
      @binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128)))
    _ => raise @protocol.Invalid("conformance.format")
  }
  Expectations::from_ion(value)
}

///|
pub fn Expectations::to_json(self : Expectations) -> Json raise {
  self.validate()
  {
    "profile": "morphir-conformance-v1",
    "version": self.version,
    "provenance": self.provenance,
    "cases": self.cases.map(c => {
      (
        {
          "id": c.call.id,
          "entry": c.call.entry,
          "arguments": c.call.arguments.map(v => v.to_json()),
          "expected": c.expected.to_json(),
        } : Json)
    }),
  }
}