///|
/// One target's observable result for a named, identical test scenario.
pub(all) struct Observation {
  target : String
  exit_code : Int
  stdout : String
  stderr : String
} derive(ToJson, FromJson)

///|
pub extend Observation with ToJson::{to_json}

///|
pub extend Observation with @json.FromJson::{from_json}

///|
/// Controls only explicitly documented normalization. Exit codes are always
/// compared; stdout is always compared; stderr can be opted out of.
pub(all) struct ComparePolicy {
  normalize_line_endings : Bool
  trim_one_final_newline : Bool
  compare_stderr : Bool
  compare_stdout_as_json : Bool
  strip_ansi_sgr : Bool
  trim_trailing_whitespace_per_line : Bool
  absolute_number_tolerance : Double
  ignored_json_pointers : Array[String]
} derive(ToJson)

///|
pub extend ComparePolicy with ToJson::{to_json}

///|
fn[T : @json.FromJson] json_object_field(
  fields : Map[String, Json],
  key : String,
  path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
  match fields.get(key) {
    Some(value) => @json.from_json(value, path=path.add_key(key))
    None =>
      raise @json.JsonDecodeError((path.add_key(key), "missing required field"))
  }
}

///|
fn[T : @json.FromJson] json_object_field_or(
  fields : Map[String, Json],
  key : String,
  path : @json.JsonPath,
  fallback : T,
) -> T raise @json.JsonDecodeError {
  match fields.get(key) {
    Some(value) => @json.from_json(value, path=path.add_key(key))
    None => fallback
  }
}

///|
pub impl @json.FromJson for ComparePolicy with fn from_json(json, path) {
  guard json is Json::Object(fields) else {
    raise @json.JsonDecodeError(
      (path, "ComparePolicy::from_json: expected object"),
    )
  }
  {
    normalize_line_endings: json_object_field(
      fields, "normalize_line_endings", path,
    ),
    trim_one_final_newline: json_object_field(
      fields, "trim_one_final_newline", path,
    ),
    compare_stderr: json_object_field(fields, "compare_stderr", path),
    compare_stdout_as_json: json_object_field(
      fields, "compare_stdout_as_json", path,
    ),
    strip_ansi_sgr: json_object_field(fields, "strip_ansi_sgr", path),
    trim_trailing_whitespace_per_line: json_object_field(
      fields, "trim_trailing_whitespace_per_line", path,
    ),
    absolute_number_tolerance: json_object_field_or(
      fields, "absolute_number_tolerance", path, 0.0,
    ),
    ignored_json_pointers: json_object_field_or(
      fields,
      "ignored_json_pointers",
      path,
      [],
    ),
  }
}

///|
pub extend ComparePolicy with @json.FromJson::{from_json}

///|
pub fn ComparePolicy::strict() -> ComparePolicy {
  {
    normalize_line_endings: true,
    trim_one_final_newline: false,
    compare_stderr: true,
    compare_stdout_as_json: false,
    strip_ansi_sgr: false,
    trim_trailing_whitespace_per_line: false,
    absolute_number_tolerance: 0.0,
    ignored_json_pointers: [],
  }
}

///|
/// Declares the targets expected for one scenario and the captured outcomes.
/// Target names are labels; callers may use official MoonBit targets or
/// explicitly named host/runtime variants.
pub(all) struct ScenarioRun {
  schema_version : Int
  name : String
  expected_targets : Array[String]
  observations : Array[Observation]
  policy : ComparePolicy
} derive(ToJson, FromJson)

///|
pub extend ScenarioRun with ToJson::{to_json}

///|
pub extend ScenarioRun with @json.FromJson::{from_json}

///|
/// An optional accepted result contract for each declared target. When a
/// contract is provided, parity and expected-result checks both have to pass.
pub(all) struct ContractScenario {
  scenario : ScenarioRun
  expectations : Array[Observation]
} derive(ToJson, FromJson)

///|
pub extend ContractScenario with ToJson::{to_json}

///|
pub extend ContractScenario with @json.FromJson::{from_json}

///|
pub enum Status {
  Pass
  Divergent
  Inconclusive
}

///|
pub(all) struct FieldDifference {
  target : String
  kind : String
  field : String
  path : String?
  reference : String
  observed : String
} derive(ToJson)

///|
pub extend FieldDifference with ToJson::{to_json}

///|
pub(all) struct ParityReport {
  scenario : String
  status : Status
  diagnostics : Array[String]
  reference_target : String?
  compared_targets : Array[String]
  missing_targets : Array[String]
  unexpected_targets : Array[String]
  duplicate_targets : Array[String]
  differences : Array[FieldDifference]
  suppressed_differences : Array[SuppressedDifference]
}

///|
/// A comparison omitted by an explicit policy rule or numeric tolerance.
pub(all) struct SuppressedDifference {
  target : String
  path : String
  reason : String
} derive(ToJson)

///|
pub extend SuppressedDifference with ToJson::{to_json}

///|
pub(all) struct SuiteReport {
  status : Status
  scenario_count : Int
  passed : Int
  divergent : Int
  inconclusive : Int
  reports : Array[ParityReport]
}