///|
/// PickleTableCell: a cell in a pickle data table.
pub(all) struct PickleTableCell {
  value : String
} derive(ToJson, FromJson)

///|
/// PickleTableRow: a row in a pickle data table.
pub(all) struct PickleTableRow {
  cells : Array[PickleTableCell]
} derive(ToJson)

///|
pub impl @json.FromJson for PickleTableRow with fn from_json(json, path) {
  let obj = expect_object(json, path, "PickleTableRow")
  PickleTableRow::{ cells: non_empty_array_field(obj, "cells", path), }
}

///|
/// PickleTable: a data table in a pickle step.
pub(all) struct PickleTable {
  /// 1 if declared before the doc string, 2 if declared after.
  argumentIndex : Int?
  rows : Array[PickleTableRow]
} derive(ToJson)

///|
pub impl @json.FromJson for PickleTable with fn from_json(json, path) {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError(
      (path, "PickleTable::from_json: expected object"),
    )
  }
  PickleTable::{
    argumentIndex: argument_index_field(obj, path),
    rows: required_field(obj, "rows", path),
  }
}

///|
/// PickleDocString: a doc string in a pickle step.
pub(all) struct PickleDocString {
  /// 1 if declared before the data table, 2 if declared after.
  argumentIndex : Int?
  mediaType : String?
  content : String
} derive(ToJson)

///|
pub impl @json.FromJson for PickleDocString with fn from_json(json, path) {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError(
      (path, "PickleDocString::from_json: expected object"),
    )
  }
  PickleDocString::{
    argumentIndex: argument_index_field(obj, path),
    mediaType: optional_field(obj, "mediaType", path),
    content: required_field(obj, "content", path),
  }
}

///|
/// PickleStepArgument: argument for a pickle step.
pub(all) struct PickleStepArgument {
  docString : PickleDocString?
  dataTable : PickleTable?
} derive(ToJson, FromJson)

///|
/// PickleStepType: type of a pickle step.
pub(all) enum PickleStepType {
  Unknown
  Context
  Action
  Outcome
}

///|
pub impl @json.FromJson for PickleStepType with fn from_json(json, path) {
  guard json is String(s) else {
    raise @json.JsonDecodeError(
      (path, "PickleStepType::from_json: expected string"),
    )
  }
  match s {
    "Unknown" => Unknown
    "Context" => Context
    "Action" => Action
    "Outcome" => Outcome
    _ =>
      raise @json.JsonDecodeError(
        (path, "PickleStepType::from_json: expected a valid pickle step type"),
      )
  }
}

///|
pub impl ToJson for PickleStepType with fn to_json(self) {
  match self {
    Unknown => "Unknown".to_json()
    Context => "Context".to_json()
    Action => "Action".to_json()
    Outcome => "Outcome".to_json()
  }
}

///|
/// PickleStep: a step in a pickle.
/// Note: JSON key is "type" (not "pickleStepType") to match upstream schema.
pub(all) struct PickleStep {
  id : String
  text : String
  astNodeIds : Array[String]
  type_ : PickleStepType?
  argument : PickleStepArgument?
}

///|
pub impl @json.FromJson for PickleStep with fn from_json(json, path) {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError(
      (path, "PickleStep::from_json: expected object"),
    )
  }
  let id : String = match obj.get("id") {
    Some(v) => @json.from_json(v, path=path.add_key("id"))
    None =>
      raise @json.JsonDecodeError((path, "PickleStep::from_json: missing id"))
  }
  let text : String = match obj.get("text") {
    Some(v) => @json.from_json(v, path=path.add_key("text"))
    None =>
      raise @json.JsonDecodeError((path, "PickleStep::from_json: missing text"))
  }
  let astNodeIds : Array[String] = non_empty_array_field(
    obj, "astNodeIds", path,
  )
  let type_ : PickleStepType? = match obj.get("type") {
    Some(Null) | None => None
    Some(v) => Some(@json.from_json(v, path=path.add_key("type")))
  }
  let argument : PickleStepArgument? = match obj.get("argument") {
    Some(Null) | None => None
    Some(v) => Some(@json.from_json(v, path=path.add_key("argument")))
  }
  PickleStep::{ id, text, astNodeIds, type_, argument, }
}

///|
pub impl ToJson for PickleStep with fn to_json(self) {
  let o : Map[String, Json] = {
    "id": self.id.to_json(),
    "text": self.text.to_json(),
    "astNodeIds": self.astNodeIds.to_json(),
  }
  match self.type_ {
    Some(t) => o.set("type", t.to_json())
    None => ()
  }
  match self.argument {
    Some(a) => o.set("argument", a.to_json())
    None => ()
  }
  o.to_json()
}

///|
/// PickleTag: a tag in a pickle.
pub(all) struct PickleTag {
  name : String
  astNodeId : String
} derive(ToJson, FromJson)

///|
/// Pickle: compiled test case from a scenario.
pub(all) struct Pickle {
  id : String
  uri : String
  name : String
  language : String
  steps : Array[PickleStep]
  tags : Array[PickleTag]
  astNodeIds : Array[String]
  location : Location?
} derive(ToJson)

///|
pub impl @json.FromJson for Pickle with fn from_json(json, path) {
  let obj = expect_object(json, path, "Pickle")
  Pickle::{
    id: required_field(obj, "id", path),
    uri: required_field(obj, "uri", path),
    name: required_field(obj, "name", path),
    language: required_field(obj, "language", path),
    steps: required_field(obj, "steps", path),
    tags: required_field(obj, "tags", path),
    astNodeIds: non_empty_array_field(obj, "astNodeIds", path),
    location: optional_field(obj, "location", path),
  }
}

///|
test "pickle step type roundtrip" {
  let json = @json.parse("\"Action\"") catch { _ => panic() }
  let pt : PickleStepType = @json.from_json(json) catch { _ => panic() }
  match pt {
    Action => ()
    _ => panic()
  }
  assert_eq(pt.to_json().stringify(), "\"Action\"")
}

///|
test "pickle roundtrip" {
  let raw = "{\"id\":\"p1\",\"uri\":\"test.feature\",\"name\":\"P\",\"language\":\"en\",\"steps\":[{\"id\":\"ps1\",\"text\":\"a step\",\"astNodeIds\":[\"s1\"]}],\"tags\":[{\"name\":\"@smoke\",\"astNodeId\":\"t1\"}],\"astNodeIds\":[\"sc1\"]}"
  let json = @json.parse(raw) catch { _ => panic() }
  let pickle : Pickle = @json.from_json(json) catch { _ => panic() }
  assert_eq(pickle.id, "p1")
  assert_eq(pickle.name, "P")
  assert_eq(pickle.steps.length(), 1)
  assert_eq(pickle.steps[0].text, "a step")
  assert_eq(pickle.tags.length(), 1)
  assert_eq(pickle.tags[0].name, "@smoke")
  let back : Pickle = @json.from_json(pickle.to_json()) catch { _ => panic() }
  assert_eq(back.id, pickle.id)
}

///|
test "pickle step argument keeps argumentIndex" {
  let raw = "{\"docString\":{\"argumentIndex\":2,\"content\":\"text\"},\"dataTable\":{\"argumentIndex\":1,\"rows\":[]}}"
  let arg : PickleStepArgument = @json.from_json(@json.parse(raw))
  assert_eq(arg.docString.map(d => d.argumentIndex), Some(Some(2)))
  assert_eq(arg.dataTable.map(t => t.argumentIndex), Some(Some(1)))
  assert_eq(arg.to_json(), @json.parse(raw))
}

///|
test "argumentIndex outside 1..2 is rejected" {
  for
    raw in [
      "{\"argumentIndex\":0,\"rows\":[]}", "{\"argumentIndex\":3,\"rows\":[]}",
    ] {
    let t : PickleTable? = Some(@json.from_json(@json.parse(raw))) catch {
      _ => None
    }
    assert_true(t is None, msg="accepted \{raw}")
  }
  for
    raw in [
      "{\"argumentIndex\":0,\"content\":\"c\"}", "{\"argumentIndex\":3,\"content\":\"c\"}",
    ] {
    let d : PickleDocString? = Some(@json.from_json(@json.parse(raw))) catch {
      _ => None
    }
    assert_true(d is None, msg="accepted \{raw}")
  }
  let t : PickleTable = @json.from_json(@json.parse("{\"rows\":[]}"))
  assert_eq(t.argumentIndex, None)
  let d : PickleDocString = @json.from_json(
    @json.parse(
      "{\"argumentIndex\":2,\"mediaType\":\"text/plain\",\"content\":\"c\"}",
    ),
  )
  assert_eq(d.argumentIndex, Some(2))
  assert_eq(d.mediaType, Some("text/plain"))
}