///|
/// 原型测试:断言式验证交互流程。
///
/// Agent(或 CI 管道)可以写"原型测试用例":
/// ```moonbit nocheck
/// test_rt.tap(160, 422).expect_navigate("home")
/// test_rt.back().expect_artboard("login")
/// test_rt.set_input("email", "test@x.com").expect_value("email", "test@x.com")
/// ```
/// 这让原型变成**可测试的**,而不仅仅是"看起来对的"。
pub(all) struct ProtoTest {
  rt : MoonVizRT
  mut passed : Int
  mut failed : Int
  mut failures : Array[String]
}

///|
/// 创建原型测试运行器
pub fn ProtoTest::create(
  project : Project,
  initial? : String = "",
) -> Result[ProtoTest, String] {
  match MoonVizRT::create(project, initial_artboard=initial) {
    Ok(rt) => Ok({ rt, passed: 0, failed: 0, failures: [], })
    Err(e) => Err(e)
  }
}

// ---------------------------------------------------------------------------
// 断言方法(链式调用风格)
// ---------------------------------------------------------------------------

///|
/// 模拟点击 → 期望导航到指定画板
pub fn ProtoTest::tap_and_expect_navigate(
  self : ProtoTest,
  x : Double,
  y : Double,
  expected : String,
) -> Bool {
  let _ = self.rt.handle_event(TapEvent(x, y))
  let actual = self.rt.current_artboard()
  if actual == expected {
    self.passed = self.passed + 1
    true
  } else {
    self.failed = self.failed + 1
    self.failures.push(
      "tap(\{x.to_int()},\{y.to_int()}) expected \{expected} but got \{actual}",
    )
    false
  }
}

///|
/// 模拟返回 → 期望回到指定画板
pub fn ProtoTest::back_and_expect(self : ProtoTest, expected : String) -> Bool {
  let _ = self.rt.handle_event(KeyEvent("back"))
  let actual = self.rt.current_artboard()
  if actual == expected {
    self.passed = self.passed + 1
    true
  } else {
    self.failed = self.failed + 1
    self.failures.push("back expected \{expected} but got \{actual}")
    false
  }
}

///|
/// 设置输入 → 期望值正确存储
pub fn ProtoTest::set_input_and_expect(
  self : ProtoTest,
  node_id : String,
  value : String,
) -> Bool {
  self.rt.set_input(node_id, value)
  let actual = self.rt.get_input(node_id)
  if actual == value {
    self.passed = self.passed + 1
    true
  } else {
    self.failed = self.failed + 1
    self.failures.push("input \{node_id} expected \{value} but got \{actual}")
    false
  }
}

///|
/// 期望当前画板无违规
pub fn ProtoTest::expect_no_violations(self : ProtoTest) -> Bool {
  let current = self.rt.current_artboard()
  match self.rt.project.artboards.get(current) {
    Some(ab) => {
      let vs = validate(ab.doc)
      if vs.length() == 0 {
        self.passed = self.passed + 1
        true
      } else {
        self.failed = self.failed + 1
        self.failures.push("\{current} has \{vs.length()} violations")
        false
      }
    }
    None => {
      self.failed = self.failed + 1
      false
    }
  }
}

///|
/// 期望渲染计划非空
pub fn ProtoTest::expect_renderable(self : ProtoTest) -> Bool {
  match self.rt.render_plan() {
    Ok(plan) =>
      if plan.commands.length() > 0 {
        self.passed = self.passed + 1
        true
      } else {
        self.failed = self.failed + 1
        self.failures.push("render plan is empty")
        false
      }
    Err(e) => {
      self.failed = self.failed + 1
      self.failures.push("render failed: \{e}")
      false
    }
  }
}

///|
/// 模拟滑动导航
pub fn ProtoTest::swipe_and_expect(
  self : ProtoTest,
  direction : String,
  expected : String,
) -> Bool {
  let _ = self.rt.handle_event(SwipeEvent(direction))
  let actual = self.rt.current_artboard()
  if actual == expected {
    self.passed = self.passed + 1
    true
  } else {
    self.failed = self.failed + 1
    self.failures.push(
      "swipe_\{direction} expected \{expected} but got \{actual}",
    )
    false
  }
}

// ---------------------------------------------------------------------------
// 测试结果
// ---------------------------------------------------------------------------

///|
/// 记录一条语法/解析失败(不计 passed)。
pub fn ProtoTest::reject_invalid(self : ProtoTest, detail : String) -> Bool {
  self.failed = self.failed + 1
  self.failures.push(detail)
  false
}

///|
pub fn ProtoTest::result(self : ProtoTest) -> String {
  let status = if self.failed == 0 { "PASS" } else { "FAIL" }
  let fj = jstr16(self.failures)
  "{\"status\":\"\{status}\",\"passed\":\{self.passed},\"failed\":\{self.failed},\"failures\":\{self.failures.length()},\"detail\":\{fj}}"
}

///|
pub fn ProtoTest::is_pass(self : ProtoTest) -> Bool {
  self.failed == 0 && self.passed > 0
}

///|
fn jstr16(parts : Array[String]) -> String {
  let escaped : Array[String] = []
  for p in parts.iter() {
    escaped.push(json_escape(p))
  }
  let sb = StringBuilder::new()
  sb.write_string("[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string("\"\{escaped[i]}\"")
  }
  sb.write_string("]")
  sb.to_string()
}

// ---------------------------------------------------------------------------
// 边界入口:测试脚本 DSL(CLI `protest` / MCP `protest` 共用)
// ---------------------------------------------------------------------------

///|
/// 原型测试脚本(分号分隔,可带空行):
///   `tap::>`    点击 → 期望导航
///   `back>`           返回 → 期望画板
///   `swipe:>`    滑动 → 期望画板(dir: left/right/up/down)
///   `set::`            输入 → 期望回读一致
///   `noviol`                        期望零违规
///   `render`                        期望可渲染
/// 例:`tap:160:422>home; back>login; noviol; render`
pub fn run_protest_script(
  project : Project,
  initial : String,
  script : String,
) -> String {
  match ProtoTest::create(project, initial~) {
    Err(e) => "{\"ok\":false,\"error\":\"\{json_escape(e)}\"}"
    Ok(pt) => {
      let lines : Array[String] = []
      for l in script.split(";").iter() {
        let t = l.trim().to_string()
        if t != "" {
          lines.push(t)
        }
      }
      // 语法错误计入 failed 并写进 failures detail——断言 DSL 的价值在
      // CI 可信,静默跳过会把拼写错误变成"少执行一条"的假 PASS
      for cmd in lines {
        let arrow = match cmd.find(">") {
          Some(i) => i
          None => -1
        }
        if cmd.starts_with("tap:") && arrow > 0 {
          let coords = cmd.substring(start=4, end=arrow).to_string()
          let expect = cmd.substring(start=arrow + 1).to_string()
          let nums : Array[String] = []
          for n in coords.split(":").iter() {
            nums.push(n.to_string())
          }
          if nums.length() >= 2 {
            let _ = pt.tap_and_expect_navigate(
              parse_double(nums[0]),
              parse_double(nums[1]),
              expect,
            )
          } else {
            let _ = pt.reject_invalid("tap 断言缺少 >期望画板:\{cmd}")
          }
        } else if cmd.starts_with("back>") {
          let expect = cmd.substring(start=5).to_string()
          let _ = pt.back_and_expect(expect)
        } else if cmd.starts_with("swipe:") && arrow > 0 {
          let dir = cmd.substring(start=6, end=arrow).to_string()
          let expect = cmd.substring(start=arrow + 1).to_string()
          let _ = pt.swipe_and_expect(dir, expect)
        } else if cmd.starts_with("set:") {
          let body = cmd.substring(start=4).to_string()
          let colon = match body.find(":") {
            Some(i) => i
            None => -1
          }
          if colon > 0 {
            let node = body.substring(start=0, end=colon).to_string()
            let value = body.substring(start=colon + 1).to_string()
            let _ = pt.set_input_and_expect(node, value)
          } else {
            let _ = pt.reject_invalid("set 断言缺少 :值:\{cmd}")
          }
        } else if cmd == "noviol" {
          let _ = pt.expect_no_violations()
        } else if cmd == "render" {
          let _ = pt.expect_renderable()
        } else {
          let _ = pt.reject_invalid("未知断言:\{cmd}")
        }
      }
      "{\"ok\":\{pt.is_pass()},\"script_lines\":\{lines.length()},\"result\":\{pt.result()}}"
    }
  }
}