///|
/// Comment: a comment in a Gherkin document.
pub struct Comment {
  location : Location
  text : String
} derive(ToJson, FromJson)

///|
/// Tag: a tag in a Gherkin document.
pub struct Tag {
  location : Location
  name : String
  id : String
} derive(ToJson, FromJson)

///|
/// TableCell: a cell in a data table row.
pub struct TableCell {
  location : Location
  value : String
} derive(ToJson, FromJson)

///|
/// TableRow: a row in a data table.
pub struct TableRow {
  location : Location
  cells : Array[TableCell]
  id : String
} derive(ToJson, FromJson)

///|
/// DataTable: a data table in a step.
pub struct DataTable {
  location : Location
  rows : Array[TableRow]
} derive(ToJson, FromJson)

///|
/// DocString: a doc string in a step.
pub struct DocString {
  location : Location
  content : String
  delimiter : String
  mediaType : String?
} derive(ToJson, FromJson)

///|
/// KeywordType: keyword type for a step.
pub enum KeywordType {
  Unknown
  Context
  Action
  Outcome
  Conjunction
}

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

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

///|
/// Step: a step in a scenario or background.
pub struct Step {
  location : Location
  keyword : String
  text : String
  id : String
  keywordType : KeywordType?
  docString : DocString?
  dataTable : DataTable?
} derive(ToJson, FromJson)

///|
/// Background: a background in a feature or rule.
pub struct Background {
  location : Location
  keyword : String
  name : String
  description : String
  steps : Array[Step]
  id : String
} derive(ToJson, FromJson)

///|
/// Examples: examples table for a scenario outline.
pub struct Examples {
  location : Location
  tags : Array[Tag]
  keyword : String
  name : String
  description : String
  tableHeader : TableRow?
  tableBody : Array[TableRow]
  id : String
} derive(ToJson, FromJson)

///|
/// Scenario: a scenario in a feature or rule.
pub struct Scenario {
  location : Location
  tags : Array[Tag]
  keyword : String
  name : String
  description : String
  steps : Array[Step]
  examples : Array[Examples]
  id : String
} derive(ToJson, FromJson)

///|
/// RuleChild: a child of a Rule (background or scenario).
pub struct RuleChild {
  background : Background?
  scenario : Scenario?
} derive(ToJson, FromJson)

///|
/// Rule: a rule grouping scenarios in a feature.
pub struct Rule {
  location : Location
  tags : Array[Tag]
  keyword : String
  name : String
  description : String
  children : Array[RuleChild]
  id : String
} derive(ToJson, FromJson)

///|
/// FeatureChild: a child of a Feature (rule, background, or scenario).
pub struct FeatureChild {
  rule : Rule?
  background : Background?
  scenario : Scenario?
} derive(ToJson, FromJson)

///|
/// Feature: a feature in a Gherkin document.
pub struct Feature {
  location : Location
  tags : Array[Tag]
  language : String
  keyword : String
  name : String
  description : String
  children : Array[FeatureChild]
} derive(ToJson, FromJson)

///|
/// GherkinDocument: parsed Gherkin AST.
pub struct GherkinDocument {
  uri : String?
  feature : Feature?
  comments : Array[Comment]
} derive(ToJson, FromJson)

///|
test "keyword type roundtrip" {
  let json = @json.parse("\"Context\"") catch { _ => panic() }
  let kt : KeywordType = @json.from_json(json) catch { _ => panic() }
  match kt {
    Context => ()
    _ => panic()
  }
  assert_eq(kt.to_json().stringify(), "\"Context\"")
}

///|
test "gherkin document minimal roundtrip" {
  let raw = "{\"comments\":[]}"
  let json = @json.parse(raw) catch { _ => panic() }
  let doc : GherkinDocument = @json.from_json(json) catch { _ => panic() }
  assert_eq(doc.comments.length(), 0)
  assert_eq(doc.uri, None)
  assert_true(doc.feature is None)
  let back : GherkinDocument = @json.from_json(doc.to_json()) catch {
    _ => panic()
  }
  assert_eq(back.comments.length(), 0)
}

///|
test "gherkin document with feature roundtrip" {
  let raw = "{\"uri\":\"test.feature\",\"comments\":[{\"location\":{\"line\":1},\"text\":\"# a comment\"}],\"feature\":{\"location\":{\"line\":2},\"tags\":[],\"language\":\"en\",\"keyword\":\"Feature\",\"name\":\"Test\",\"description\":\"\",\"children\":[]}}"
  let json = @json.parse(raw) catch { _ => panic() }
  let doc : GherkinDocument = @json.from_json(json) catch { _ => panic() }
  assert_eq(doc.uri, Some("test.feature"))
  assert_eq(doc.comments.length(), 1)
  assert_eq(doc.comments[0].text, "# a comment")
  guard doc.feature is Some(f)
  assert_eq(f.name, "Test")
  assert_eq(f.language, "en")
  let back : GherkinDocument = @json.from_json(doc.to_json()) catch {
    _ => panic()
  }
  guard back.feature is Some(f2)
  assert_eq(f2.name, "Test")
}

///|
test "gherkin document with scenario roundtrip" {
  let raw = "{\"comments\":[],\"feature\":{\"location\":{\"line\":1},\"tags\":[],\"language\":\"en\",\"keyword\":\"Feature\",\"name\":\"F\",\"description\":\"\",\"children\":[{\"scenario\":{\"location\":{\"line\":3},\"tags\":[{\"location\":{\"line\":2},\"name\":\"@smoke\",\"id\":\"t1\"}],\"keyword\":\"Scenario\",\"name\":\"S\",\"description\":\"\",\"steps\":[{\"location\":{\"line\":4},\"keyword\":\"Given \",\"text\":\"a step\",\"id\":\"s1\",\"keywordType\":\"Context\"}],\"examples\":[],\"id\":\"sc1\"}}]}}"
  let json = @json.parse(raw) catch { _ => panic() }
  let doc : GherkinDocument = @json.from_json(json) catch { _ => panic() }
  guard doc.feature is Some(f)
  assert_eq(f.children.length(), 1)
  guard f.children[0].scenario is Some(sc)
  assert_eq(sc.name, "S")
  assert_eq(sc.tags.length(), 1)
  assert_eq(sc.tags[0].name, "@smoke")
  assert_eq(sc.steps.length(), 1)
  assert_eq(sc.steps[0].text, "a step")
  guard sc.steps[0].keywordType is Some(kt)
  match kt {
    Context => ()
    _ => panic()
  }
  let back : GherkinDocument = @json.from_json(doc.to_json()) catch {
    _ => panic()
  }
  guard back.feature is Some(f2)
  guard f2.children[0].scenario is Some(sc2)
  assert_eq(sc2.name, "S")
}