///|
fn normalize_text_for_compare(text : String) -> String {
  // Parser/printer are whitespace-insensitive; normalize by removing all whitespace.
  let out = StringBuilder::new()
  for c in text {
    if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
fn wast_ascii_lower(input : String) -> String {
  let out = StringBuilder::new()
  for c in input {
    if c >= 'A' && c <= 'Z' {
      out.write_char((c.to_int() + 32).unsafe_to_char())
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub struct WastRoundtripFuzzStats {
  profile : String
  seed : UInt64
  attempts : Int32
  successes : Int32
  stable : Int32
  script_iterations : Int32
  module_commands : Int32
  rendered_scripts : Int32
  generation_mode : String
  valid_lowered_modules : Int32
  static_assertions : Int32
  feature_stats : WastArbitraryFeatureStats
} derive(Eq, Debug)

///|
fn wast_roundtrip_parse_profile(
  profile : String,
) -> Result[(String, String, Int32, Int32, Int32, Int32, Int32), String] {
  let parts : Array[String] = []
  for raw_part in profile.trim().to_owned().split("+") {
    let part = wast_ascii_lower(raw_part.trim().to_owned())
    if part.length() > 0 {
      parts.push(part)
    }
  }
  let base = if parts.length() == 0 { "smoke" } else { parts[0] }
  let (max_attempts, success_target, min_successes, min_stable, script_iters) = match
    base {
    "smoke" => (120, 16, 8, 1, 24)
    "ci" => (600, 80, 20, 2, 64)
    "stress" => (2400, 320, 80, 8, 256)
    _ =>
      return Err(
        "unknown wast fuzz profile '\{profile}'; expected smoke|ci|stress with optional +parser-stress|+valid-only|+static-assertions|+scripts|+module-only",
      )
  }
  let mode = if parts.length() > 1 { parts[1] } else { "parser-stress" }
  match mode {
    "parser-stress"
    | "valid-only"
    | "static-assertions"
    | "scripts"
    | "module-only" =>
      Ok(
        (
          base, mode, max_attempts, success_target, min_successes, min_stable, script_iters,
        ),
      )
    _ =>
      Err(
        "unknown WAST generation mode '\{mode}'; expected parser-stress|valid-only|static-assertions|scripts|module-only",
      )
  }
}

///|
fn wast_roundtrip_valid_only_module(st : RandomState) -> Module {
  Module::{
    id: None,
    fields: [
      FuncField({
        id: Some("$valid0"),
        annotations: [],
        exports: [Export("valid0")],
        type_use: {
          type_index: None,
          func_type: { params: [], results: [I32], },
          param_ids: [],
        },
        locals: [],
        body: [Const(I32Const, "\{st.next_uint() % 1024}")],
      }),
    ],
  }
}

///|
fn wast_roundtrip_static_assertion_script(st : RandomState) -> WastScript {
  let import_idx = st.next_uint() % 32
  {
    commands: [
      AssertUnlinkable(
        Inline(Module::{
          id: None,
          fields: [
            ImportField({
              module_name: "env",
              field_name: "missing\{import_idx}",
              desc: ImportDesc::Func({
                type_index: None,
                func_type: { params: [], results: [], },
                param_ids: [],
              }),
              id: Some("$imp"),
              annotations: [],
            }),
          ],
        }),
        "missing import",
      ),
    ],
  }
}

///|
fn wast_roundtrip_count_static_assertions(script : WastScript) -> Int32 {
  let mut count = 0
  for cmd in script.commands {
    match cmd {
      AssertMalformed(_, _) | AssertInvalid(_, _) | AssertUnlinkable(_, _) =>
        count += 1
      _ => ()
    }
  }
  count
}

///|
pub fn run_wast_roundtrip_fuzz(
  profile : String,
  seed : UInt64,
) -> Result[WastRoundtripFuzzStats, String] {
  let (
    normalized,
    generation_mode,
    max_attempts,
    success_target,
    min_successes,
    min_stable,
    script_iters,
  ) = match wast_roundtrip_parse_profile(profile) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let state = @splitmix.new(seed~)
  let mut successes = 0
  let mut stable = 0
  let mut attempts = 0
  let mut valid_lowered_modules = 0
  let mut feature_stats = wast_arbitrary_empty_feature_stats()
  while attempts < max_attempts && successes < success_target {
    attempts += 1
    let wmod : Module = if generation_mode == "valid-only" {
      wast_roundtrip_valid_only_module(state)
    } else {
      Arbitrary::arbitrary(5, state)
    }
    let wmod_feature_stats = wast_arbitrary_feature_stats(wmod)
    match module_to_wast(wmod) {
      Err(_) => continue
      Ok(printed) => {
        let parsed = match wast_to_module(printed) {
          Err(_) => continue
          Ok(parsed) => parsed
        }
        let printed_round = match module_to_wast(parsed) {
          Err(_) => continue
          Ok(text) => text
        }
        if generation_mode == "valid-only" {
          let lowered = match wast_to_binary_module(printed) {
            Err(_) => continue
            Ok(v) => v
          }
          match @validate.validate_module(lowered) {
            Err(_) => continue
            Ok(_) => valid_lowered_modules += 1
          }
        }
        successes += 1
        feature_stats = wast_arbitrary_add_feature_stats(
          feature_stats, wmod_feature_stats,
        )
        if normalize_text_for_compare(printed) ==
          normalize_text_for_compare(printed_round) {
          stable += 1
        }
      }
    }
  }
  if successes < min_successes {
    return Err(
      "wast fuzz produced too few successful roundtrips: profile=\{normalized} successes=\{successes} min=\{min_successes} attempts=\{attempts}",
    )
  }
  if stable < min_stable {
    return Err(
      "wast fuzz produced too few stable roundtrips: profile=\{normalized} stable=\{stable} min=\{min_stable} successes=\{successes}",
    )
  }

  let script_state = @splitmix.new(seed=seed ^ 0xABCUL)
  let actual_script_iters = if generation_mode == "module-only" {
    0
  } else {
    script_iters
  }
  let mut module_commands = 0
  let mut rendered_scripts = 0
  let mut static_assertions = 0
  for _ in 0.. {
        ignore(wast_to_script(text))
        rendered_scripts += 1
      }
      Err(_) => ()
    }
  }
  if generation_mode != "module-only" {
    if generation_mode == "scripts" && module_commands <= 0 {
      return Err(
        "wast fuzz generated no module commands: profile=\{normalized} mode=\{generation_mode} script_iterations=\{actual_script_iters}",
      )
    }
    if rendered_scripts <= 0 {
      return Err(
        "wast fuzz rendered no scripts: profile=\{normalized} mode=\{generation_mode} script_iterations=\{actual_script_iters}",
      )
    }
  }
  Ok(WastRoundtripFuzzStats::{
    profile: normalized,
    seed,
    attempts,
    successes,
    stable,
    script_iterations: actual_script_iters,
    module_commands,
    rendered_scripts,
    generation_mode,
    valid_lowered_modules,
    static_assertions,
    feature_stats,
  })
}

///|
test "wast module conversion basic roundtrip" {
  let src =
    #| (module
    #| )
  let mod1 = match wast_to_module(src) {
    Ok(m) => m
    Err(msg) => {
      assert_true(false, msg~)
      { id: None, fields: [], }
    }
  }
  let text1 = module_to_wast(mod1).unwrap()
  let mod2 = wast_to_module(text1).unwrap()
  let text2 = module_to_wast(mod2).unwrap()
  @debug.assert_eq(
    normalize_text_for_compare(text1),
    normalize_text_for_compare(text2),
  )
}

///|
test "run_wast_roundtrip_fuzz reports WAST arbitrary FZG mirror counters" {
  match run_wast_roundtrip_fuzz("smoke", 0x5eedUL) {
    Ok(stats) => {
      @debug.assert_eq(stats.successes, 16)
      @debug.assert_eq(stats.feature_stats.gc_type_fields, 16)
      @debug.assert_eq(stats.feature_stats.exported_tags, 16)
      @debug.assert_eq(stats.feature_stats.descriptor_ref_ops, 32)
      @debug.assert_eq(stats.feature_stats.bulk_memory_ops, 64)
      @debug.assert_eq(stats.feature_stats.bulk_table_ops, 64)
      @debug.assert_eq(stats.feature_stats.tail_call_ops, 48)
      @debug.assert_eq(stats.feature_stats.exception_ops, 32)
      @debug.assert_eq(stats.feature_stats.simd_const_ops, 16)
      assert_true(
        stats.feature_stats.control_ops >
        stats.feature_stats.tail_call_ops + stats.feature_stats.exception_ops,
        msg="random WAST bodies should add control-family opcode coverage beyond the widened prelude",
      )
      assert_true(
        stats.feature_stats.numeric_ops > stats.feature_stats.simd_const_ops,
        msg="random WAST bodies should add numeric opcode coverage beyond the widened prelude",
      )
      assert_true(
        stats.feature_stats.memory_ops > stats.feature_stats.bulk_memory_ops,
        msg="random WAST bodies should add memory opcode coverage beyond the widened prelude",
      )
      assert_true(
        stats.feature_stats.table_ops > stats.feature_stats.bulk_table_ops,
        msg="random WAST bodies should add table opcode coverage beyond the widened prelude",
      )
      assert_true(
        stats.feature_stats.reference_ops >
        stats.feature_stats.descriptor_ref_ops,
        msg="random WAST bodies should add reference opcode coverage beyond the widened prelude",
      )
    }
    Err(msg) => fail("expected smoke WAST roundtrip success: \{msg}")
  }
}

///|
test "run_wast_roundtrip_fuzz supports WAST generation modes" {
  match run_wast_roundtrip_fuzz("smoke+valid-only", 0x5eedUL) {
    Ok(stats) => {
      @debug.assert_eq(stats.profile, "smoke")
      @debug.assert_eq(stats.generation_mode, "valid-only")
      @debug.assert_eq(stats.successes, 16)
      @debug.assert_eq(stats.valid_lowered_modules, stats.successes)
    }
    Err(msg) => fail("expected valid-only WAST mode success: \{msg}")
  }
  match run_wast_roundtrip_fuzz("smoke+module-only", 0x5eedUL) {
    Ok(stats) => {
      @debug.assert_eq(stats.generation_mode, "module-only")
      @debug.assert_eq(stats.script_iterations, 0)
      @debug.assert_eq(stats.module_commands, 0)
      @debug.assert_eq(stats.rendered_scripts, 0)
      @debug.assert_eq(stats.successes, 16)
    }
    Err(msg) => fail("expected module-only WAST mode success: \{msg}")
  }
  match run_wast_roundtrip_fuzz("smoke+static-assertions", 0x5eedUL) {
    Ok(stats) => {
      @debug.assert_eq(stats.generation_mode, "static-assertions")
      assert_true(stats.static_assertions > 0)
      assert_true(stats.rendered_scripts > 0)
    }
    Err(msg) => fail("expected static-assertions WAST mode success: \{msg}")
  }
  match run_wast_roundtrip_fuzz("smoke+scripts", 0x5eedUL) {
    Ok(stats) => {
      @debug.assert_eq(stats.generation_mode, "scripts")
      assert_true(stats.module_commands > 0)
      assert_true(stats.rendered_scripts > 0)
    }
    Err(msg) => fail("expected scripts WAST mode success: \{msg}")
  }
}

///|
test "run_wast_roundtrip_fuzz rejects unknown profile" {
  match run_wast_roundtrip_fuzz("not-a-profile", 0x5eedUL) {
    Ok(_) => fail("expected unknown profile error")
    Err(msg) => assert_true(msg.contains("unknown wast fuzz profile"))
  }
  match run_wast_roundtrip_fuzz("smoke+not-a-mode", 0x5eedUL) {
    Ok(_) => fail("expected unknown WAST generation mode error")
    Err(msg) => assert_true(msg.contains("unknown WAST generation mode"))
  }
}