///|
/// A typed value extracted from step text, pattern-matchable for built-in types.
pub(all) enum StepValue {
  IntVal(Int)
  FloatVal(Double)
  DoubleVal(Double)
  LongVal(Int64)
  ByteVal(Byte)
  ShortVal(Int)
  StringVal(String)
  WordVal(String)
  AnonymousVal(String)
  BigDecimalVal(@decimal.Decimal)
  BigIntegerVal(BigInt)
  CustomVal(@any.Any)
  DocStringVal(DocString)
  DataTableVal(DataTable)
}

///|
impl Show for StepValue with output(self, logger) {
  match self {
    IntVal(v) => logger.write_string("IntVal(\{v})")
    FloatVal(v) => logger.write_string("FloatVal(\{v})")
    DoubleVal(v) => logger.write_string("DoubleVal(\{v})")
    LongVal(v) => logger.write_string("LongVal(\{v})")
    ByteVal(v) => logger.write_string("ByteVal(\{v})")
    ShortVal(v) => logger.write_string("ShortVal(\{v})")
    StringVal(v) => logger.write_string("StringVal(\{v})")
    WordVal(v) => logger.write_string("WordVal(\{v})")
    AnonymousVal(v) => logger.write_string("AnonymousVal(\{v})")
    BigDecimalVal(v) => logger.write_string("BigDecimalVal(\{v})")
    BigIntegerVal(v) => logger.write_string("BigIntegerVal(\{v})")
    CustomVal(any) => logger.write_string("CustomVal(<\{any.type_name()}>)")
    DocStringVal(d) => {
      let mt = match d.media_type {
        Some(t) => t
        None => ""
      }
      logger.write_string("DocStringVal(media_type=\{mt})")
    }
    DataTableVal(t) =>
      logger.write_string("DataTableVal(\{t.row_count()}x\{t.col_count()})")
  }
}

///|
impl Eq for StepValue with equal(self, other) -> Bool {
  match (self, other) {
    (IntVal(a), IntVal(b)) => a == b
    (FloatVal(a), FloatVal(b)) => a == b
    (DoubleVal(a), DoubleVal(b)) => a == b
    (LongVal(a), LongVal(b)) => a == b
    (ByteVal(a), ByteVal(b)) => a == b
    (ShortVal(a), ShortVal(b)) => a == b
    (StringVal(a), StringVal(b)) => a == b
    (WordVal(a), WordVal(b)) => a == b
    (AnonymousVal(a), AnonymousVal(b)) => a == b
    (BigDecimalVal(a), BigDecimalVal(b)) => a == b
    (BigIntegerVal(a), BigIntegerVal(b)) => a == b
    (CustomVal(_), CustomVal(_)) => true
    (DocStringVal(a), DocStringVal(b)) => a == b
    (DataTableVal(a), DataTableVal(b)) => a == b
    _ => false
  }
}

///|
/// A buffered attachment waiting to be emitted as an envelope.
pub(all) enum PendingAttachment {
  Embedded(
    body~ : String,
    encoding~ : @cucumber_messages.AttachmentContentEncoding,
    media_type~ : String,
    file_name~ : String?
  )
  External(url~ : String, media_type~ : String)
}

///|
impl Show for PendingAttachment with output(self, logger) {
  match self {
    Embedded(body~, encoding~, media_type~, file_name~) => {
      let enc_str = encoding.to_json().stringify()
      logger.write_string(
        "Embedded(body=\{body}, encoding=\{enc_str}, media_type=\{media_type}, file_name=\{file_name})",
      )
    }
    External(url~, media_type~) =>
      logger.write_string("External(url=\{url}, media_type=\{media_type})")
  }
}

///|
impl Eq for PendingAttachment with equal(self, other) -> Bool {
  match (self, other) {
    (
      Embedded(body=b1, encoding=e1, media_type=m1, file_name=f1),
      Embedded(body=b2, encoding=e2, media_type=m2, file_name=f2),
    ) =>
      b1 == b2 &&
      e1.to_json().stringify() == e2.to_json().stringify() &&
      m1 == m2 &&
      f1 == f2
    (External(url=u1, media_type=m1), External(url=u2, media_type=m2)) =>
      u1 == u2 && m1 == m2
    _ => false
  }
}

///|
/// A step argument carrying both the typed value and the original matched text.
pub(all) struct StepArg {
  value : StepValue
  raw : String
} derive(Show, Eq)

///|
/// Convert a cucumber-expressions Param to a StepArg.
pub fn StepArg::from_param(param : @cucumber_expressions.Param) -> StepArg {
  let value : StepValue = match param.value {
    @cucumber_expressions.ParamValue::IntVal(n) => IntVal(n)
    @cucumber_expressions.ParamValue::FloatVal(f) => FloatVal(f)
    @cucumber_expressions.ParamValue::DoubleVal(f) => DoubleVal(f)
    @cucumber_expressions.ParamValue::LongVal(n) => LongVal(n)
    @cucumber_expressions.ParamValue::ByteVal(b) => ByteVal(b)
    @cucumber_expressions.ParamValue::ShortVal(n) => ShortVal(n)
    @cucumber_expressions.ParamValue::StringVal(s) => StringVal(s)
    @cucumber_expressions.ParamValue::WordVal(s) => WordVal(s)
    @cucumber_expressions.ParamValue::AnonymousVal(s) => AnonymousVal(s)
    @cucumber_expressions.ParamValue::BigDecimalVal(d) => BigDecimalVal(d)
    @cucumber_expressions.ParamValue::BigIntegerVal(bi) => BigIntegerVal(bi)
    @cucumber_expressions.ParamValue::CustomVal(any) => CustomVal(any)
  }
  { value, raw: param.raw }
}

///|
/// Step execution context. Wraps matched arguments with scenario/step metadata
/// and an attachment buffer.
pub(all) struct Ctx {
  priv step_args : Array[StepArg]
  priv scenario_info : ScenarioInfo
  priv step_info : StepInfo
  priv attachments : Array[PendingAttachment]
}

///|
/// Create a new Ctx.
pub fn Ctx::new(
  args : Array[StepArg],
  scenario : ScenarioInfo,
  step : StepInfo,
) -> Ctx {
  { step_args: args, scenario_info: scenario, step_info: step, attachments: [] }
}

///|
/// Index access to step arguments.
pub fn Ctx::op_get(self : Ctx, index : Int) -> StepArg {
  self.step_args[index]
}

///|
/// Explicit index access to a step argument.
pub fn Ctx::arg(self : Ctx, index : Int) -> StepArg {
  self.step_args[index]
}

///|
/// View of all step arguments.
pub fn Ctx::args(self : Ctx) -> ArrayView[StepArg] {
  self.step_args[:]
}

///|
/// Convenience accessor: returns the StepValue at the given argument index.
pub fn Ctx::value(self : Ctx, index : Int) -> StepValue {
  self.step_args[index].value
}

///|
/// Scenario metadata (feature name, scenario name, tags).
pub fn Ctx::scenario(self : Ctx) -> ScenarioInfo {
  self.scenario_info
}

///|
/// Step metadata (keyword, text).
pub fn Ctx::step(self : Ctx) -> StepInfo {
  self.step_info
}

///|
/// Trait for any context that can buffer attachments.
pub trait Attachable {
  attach(Self, String, String, file_name? : String) -> Unit
  attach_bytes(Self, Bytes, String, file_name? : String) -> Unit
  attach_url(Self, String, String) -> Unit
  pending_attachments(Self) -> Array[PendingAttachment]
}

///|
/// Parse an AttachmentContentEncoding from a JSON string literal.
/// Workaround: enum constructors from external packages are not directly
/// accessible in MoonBit, so we round-trip through JSON.
fn parse_encoding(s : String) -> @cucumber_messages.AttachmentContentEncoding {
  @json.from_json(@json.parse(s) catch { _ => panic() }) catch {
    _ => panic()
  }
}

///|
/// Attach text content to the current step/hook.
pub fn Ctx::attach(
  self : Ctx,
  body : String,
  media_type : String,
  file_name? : String,
) -> Unit {
  let encoding = parse_encoding("\"IDENTITY\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
/// Attach binary content (Base64 encoded) to the current step/hook.
pub fn Ctx::attach_bytes(
  self : Ctx,
  data : Bytes,
  media_type : String,
  file_name? : String,
) -> Unit {
  let body = @base64.encode(data[:])
  let encoding = parse_encoding("\"BASE64\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
/// Attach an external URL reference to the current step/hook.
pub fn Ctx::attach_url(self : Ctx, url : String, media_type : String) -> Unit {
  self.attachments.push(PendingAttachment::External(url~, media_type~))
}

///|
/// Access buffered attachments (for executor draining).
pub fn Ctx::pending_attachments(self : Ctx) -> Array[PendingAttachment] {
  self.attachments
}

///|
/// Result of matching step text against the registry.
pub(all) enum StepMatchResult {
  Matched(StepDef, Array[StepArg])
  Undefined(
    step_text~ : String,
    keyword~ : String,
    snippet~ : String,
    suggestions~ : Array[String]
  )
}

///|
/// Information about a scenario being executed.
pub(all) struct ScenarioInfo {
  feature_name : String
  scenario_name : String
  tags : Array[String]
} derive(Show, Eq)

///|
/// Information about a step being executed.
pub(all) struct StepInfo {
  keyword : String
  text : String
} derive(Show, Eq)

///|
/// Structured error reported to after-hooks.
pub(all) enum HookError {
  StepFailed(step~ : String, keyword~ : StepKeyword, message~ : String)
  ScenarioFailed(
    feature_name~ : String,
    scenario_name~ : String,
    message~ : String
  )
} derive(Show, Eq)

///|
/// Result passed to after-hooks indicating pass/fail with structured errors.
pub(all) enum HookResult {
  Passed
  Failed(Array[HookError])
} derive(Show, Eq)

///|
/// Hook context for test run-level hooks (before/after_test_run).
pub(all) struct RunHookCtx {
  priv attachments : Array[PendingAttachment]
}

///|
pub fn RunHookCtx::new() -> RunHookCtx {
  { attachments: [] }
}

///|
pub fn RunHookCtx::pending_attachments(
  self : RunHookCtx,
) -> Array[PendingAttachment] {
  self.attachments
}

///|
pub fn RunHookCtx::attach(
  self : RunHookCtx,
  body : String,
  media_type : String,
  file_name? : String,
) -> Unit {
  let encoding = parse_encoding("\"IDENTITY\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn RunHookCtx::attach_bytes(
  self : RunHookCtx,
  data : Bytes,
  media_type : String,
  file_name? : String,
) -> Unit {
  let body = @base64.encode(data[:])
  let encoding = parse_encoding("\"BASE64\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn RunHookCtx::attach_url(
  self : RunHookCtx,
  url : String,
  media_type : String,
) -> Unit {
  self.attachments.push(PendingAttachment::External(url~, media_type~))
}

///|
/// Hook context for test case-level hooks (before/after_test_case).
pub(all) struct CaseHookCtx {
  priv scenario_info : ScenarioInfo
  priv attachments : Array[PendingAttachment]
}

///|
pub fn CaseHookCtx::new(scenario : ScenarioInfo) -> CaseHookCtx {
  { scenario_info: scenario, attachments: [] }
}

///|
pub fn CaseHookCtx::scenario(self : CaseHookCtx) -> ScenarioInfo {
  self.scenario_info
}

///|
pub fn CaseHookCtx::pending_attachments(
  self : CaseHookCtx,
) -> Array[PendingAttachment] {
  self.attachments
}

///|
pub fn CaseHookCtx::attach(
  self : CaseHookCtx,
  body : String,
  media_type : String,
  file_name? : String,
) -> Unit {
  let encoding = parse_encoding("\"IDENTITY\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn CaseHookCtx::attach_bytes(
  self : CaseHookCtx,
  data : Bytes,
  media_type : String,
  file_name? : String,
) -> Unit {
  let body = @base64.encode(data[:])
  let encoding = parse_encoding("\"BASE64\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn CaseHookCtx::attach_url(
  self : CaseHookCtx,
  url : String,
  media_type : String,
) -> Unit {
  self.attachments.push(PendingAttachment::External(url~, media_type~))
}

///|
/// Hook context for test step-level hooks (before/after_test_step).
pub(all) struct StepHookCtx {
  priv scenario_info : ScenarioInfo
  priv step_info : StepInfo
  priv attachments : Array[PendingAttachment]
}

///|
pub fn StepHookCtx::new(
  scenario : ScenarioInfo,
  step : StepInfo,
) -> StepHookCtx {
  { scenario_info: scenario, step_info: step, attachments: [] }
}

///|
pub fn StepHookCtx::scenario(self : StepHookCtx) -> ScenarioInfo {
  self.scenario_info
}

///|
pub fn StepHookCtx::step(self : StepHookCtx) -> StepInfo {
  self.step_info
}

///|
pub fn StepHookCtx::pending_attachments(
  self : StepHookCtx,
) -> Array[PendingAttachment] {
  self.attachments
}

///|
pub fn StepHookCtx::attach(
  self : StepHookCtx,
  body : String,
  media_type : String,
  file_name? : String,
) -> Unit {
  let encoding = parse_encoding("\"IDENTITY\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn StepHookCtx::attach_bytes(
  self : StepHookCtx,
  data : Bytes,
  media_type : String,
  file_name? : String,
) -> Unit {
  let body = @base64.encode(data[:])
  let encoding = parse_encoding("\"BASE64\"")
  self.attachments.push(
    PendingAttachment::Embedded(body~, encoding~, media_type~, file_name~),
  )
}

///|
pub fn StepHookCtx::attach_url(
  self : StepHookCtx,
  url : String,
  media_type : String,
) -> Unit {
  self.attachments.push(PendingAttachment::External(url~, media_type~))
}

///|
/// Immutable wrapper around an array of cell values in a DataTable row.
pub struct Cells {
  priv data : Array[String]

  fn new(data : Array[String]) -> Cells
} derive(Show, Eq)

///|
fn Cells::new(data : Array[String]) -> Cells {
  { data, }
}

///|
pub fn Cells::values(self : Cells) -> ArrayView[String] {
  self.data[:]
}

///|
pub fn Cells::get(self : Cells, index : Int) -> String {
  self.data[index]
}

///|
pub fn Cells::length(self : Cells) -> Int {
  self.data.length()
}

///|
/// A single row in a DataTable, wrapping its cell values in an immutable Cells.
pub(all) struct Row {
  cells : Cells

  fn new(cells : Array[String]) -> Row
} derive(Show, Eq)

///|
fn Row::new(cells : Array[String]) -> Row {
  { cells: Cells(cells) }
}

///|
/// Immutable wrapper around an array of rows in a DataTable.
pub struct Rows {
  priv data : Array[Row]

  fn new(data : Array[Row]) -> Rows
} derive(Show, Eq)

///|
fn Rows::new(data : Array[Row]) -> Rows {
  { data, }
}

///|
pub fn Rows::values(self : Rows) -> ArrayView[Row] {
  self.data[:]
}

///|
pub fn Rows::get(self : Rows, index : Int) -> Row {
  self.data[index]
}

///|
pub fn Rows::length(self : Rows) -> Int {
  self.data.length()
}

///|
/// A named column in a DataTable, derived from the header row.
pub(all) struct Column {
  name : String
  index : Int

  fn new(name~ : String, index~ : Int) -> Column
} derive(Show, Eq)

///|
fn Column::new(name~ : String, index~ : Int) -> Column {
  { name, index }
}

///|
/// Immutable wrapper around an array of columns in a DataTable.
pub struct Columns {
  priv data : Array[Column]

  fn new(data : Array[Column]) -> Columns
} derive(Show, Eq)

///|
fn Columns::new(data : Array[Column]) -> Columns {
  { data, }
}

///|
pub fn Columns::values(self : Columns) -> ArrayView[Column] {
  self.data[:]
}

///|
pub fn Columns::get(self : Columns, index : Int) -> Column {
  self.data[index]
}

///|
pub fn Columns::length(self : Columns) -> Int {
  self.data.length()
}

///|
pub fn Columns::find(self : Columns, name : String) -> Column? {
  for col in self.data {
    if col.name == name {
      return Some(col)
    }
  }
  None
}

///|
/// A DataTable from a Gherkin step. First row is treated as column headers.
pub(all) struct DataTable {
  rows : Rows
  columns : Columns

  fn new(raw_rows : Array[Array[String]]) -> DataTable
} derive(Show, Eq)

///|
fn DataTable::new(raw_rows : Array[Array[String]]) -> DataTable {
  let columns = if raw_rows.length() > 0 {
    Columns(raw_rows[0].mapi(fn(i, name) { Column(name~, index=i) }))
  } else {
    Columns([])
  }
  let rows = Rows(raw_rows.map(fn(cells) { Row(cells) }))
  { rows, columns }
}

///|
/// Construct a DataTable from raw row data (public factory).
pub fn DataTable::from_rows(raw_rows : Array[Array[String]]) -> DataTable {
  DataTable::new(raw_rows)
}

///|
pub fn DataTable::row_count(self : DataTable) -> Int {
  self.rows.length()
}

///|
pub fn DataTable::col_count(self : DataTable) -> Int {
  self.columns.length()
}

///|
pub fn DataTable::as_maps(self : DataTable) -> Array[Map[String, String]] {
  let result : Array[Map[String, String]] = []
  if self.rows.length() <= 1 {
    return result
  }
  let headers = self.columns.values()
  for i = 1; i < self.rows.length(); i = i + 1 {
    let row = self.rows.get(i)
    let map : Map[String, String] = {}
    for j = 0; j < headers.length(); j = j + 1 {
      map[headers[j].name] = row.cells.get(j)
    }
    result.push(map)
  }
  result
}

///|
/// A DocString from a Gherkin step — multi-line text with optional media type.
pub(all) struct DocString {
  content : String
  media_type : String?

  fn new(content~ : String, media_type? : String) -> DocString
} derive(Show, Eq)

///|
fn DocString::new(content~ : String, media_type? : String) -> DocString {
  { content, media_type }
}