// ---------------------------------------------------------------------------
// issue #25 / #26:交互与内容完整性 QA(critique 并入 norms 维度,软路径)
//
// 与 norms 的分工:norms 管视觉工艺(留白/圆角/触控/对比度),本模块管
// 「原型可交互性」——可交互组件必须有去向、内容不能是占位空壳、组件必须
// 来自组件库。冲突类(同一节点多 flow / 可点热区相交)另行走 AgentGate
// 硬门(qa_agent_conflicts),检测项与硬门共用同一判据。
// ---------------------------------------------------------------------------

///|
/// 可交互族组件(无 flow/交互标记时必须报告)。
pub fn qa_is_interactive_component(c : String) -> Bool {
  c == "button" ||
  c == "chip" ||
  c == "fab" ||
  c == "tab_bar" ||
  c == "list_item"
}

///|
/// 占位符残留判定(issue #25-B5):TBD/TODO/Lorem/xxx/Item 1/项目一/示例文本/占位。
pub fn qa_is_placeholder_text(t : String) -> Bool {
  let s = t.trim().to_lower()
  if s == "" {
    return false
  }
  s.contains("tbd") ||
  s.contains("todo") ||
  s.contains("lorem") ||
  s.contains("xxx") ||
  s.contains("item 1") ||
  s.contains("项目一") ||
  s.contains("示例文本") ||
  s.contains("占位")
}

///|
/// mock 容器族:空壳(无子节点、无文本)视为缺示例数据。
pub fn qa_is_mock_container(c : String) -> Bool {
  c == "list_item" || c == "table" || c == "card"
}

///|
/// 组件必须来自组件库(builtin 或用户组件注册表)。
pub fn qa_component_known(self : Project, c : String) -> Bool {
  // 结构组件(画布 frame)与空名不算「非组件库组件」——泛型容器合法。
  if c == "frame" || c == "" {
    return true
  }
  if self.components.contains(c) {
    return true
  }
  self.user_components.contains(c)
}

///|
/// 交互去向判定(issue #25-A1):flow tap: / 名字里的交互标记(⚡ 或
/// [state:)/ 返回类节点(内置返回兜底)都算有去向。
pub fn qa_is_bound(self : Project, artboard : String, n : Node) -> Bool {
  if is_back_like_node(n) {
    return true
  }
  if n.name.contains("⚡") || n.name.contains("[state:") {
    return true
  }
  for f in self.flows.iter() {
    if f.from == artboard && f.trigger == "tap:" + n.id {
      return true
    }
  }
  false
}

///|
/// critique 第 9 维追加项(issue #25/#26 软路径检测):
/// 1) 可交互组件无去向(无 flow/标记/返回兜底)
/// 2) 占位符残留文本
/// 3) 空壳 list/table/card(无示例条目)
/// 4) 非组件库组件(generic_node 当组件用)
pub fn qa_issues(self : Project, artboard~ : String) -> Array[String] {
  let issues : Array[String] = []
  // issue #24 要求 2:spec 组件白名单/禁用项的整篇检视并入 QA
  for v in self.design_spec_component_violations(artboard~).iter() {
    if issues.length() >= 12 {
      break
    }
    issues.push(v)
  }
  match self.artboards.get(artboard) {
    None => ()
    Some(ab) =>
      for id in ab.doc.nodes.keys() {
        if issues.length() >= 8 {
          break
        }
        // 画布 root 是结构节点,不参与交互/内容 QA
        if id == ab.doc.root_id {
          continue
        }
        match ab.doc.nodes.get(id) {
          None => ()
          Some(n) => {
            if qa_is_interactive_component(n.component) &&
              !self.qa_is_bound(artboard, n) {
              issues.push(
                "\{json_escape(id)}(\{n.component})可交互组件无去向:无 flow/交互标记、非返回类 → 绑 flow 或改非交互组件(critique 检测项)",
              )
              continue
            }
            if qa_is_placeholder_text(n.text) {
              issues.push(
                "\{json_escape(id)}(\{n.component})占位符残留:\{json_escape(n.text)} → 换语义化真实示例",
              )
              continue
            }
            if qa_is_mock_container(n.component) &&
              n.children.length() == 0 &&
              n.text.length() == 0 {
              issues.push(
                "\{json_escape(id)}(\{n.component})空壳容器无示例条目 → 填 ≥2 条语义示例或改 empty_state 变体",
              )
              continue
            }
            if !self.qa_component_known(n.component) {
              issues.push(
                "\{json_escape(id)} 使用非组件库组件「\{n.component}」→ 换注册表组件(generic_node 仅可用于纯容器/装饰)",
              )
            }
          }
        }
      }
  }
  issues
}

// ---------------------------------------------------------------------------
// issue #25-A3 / #26-A3:交互冲突硬门(AgentGate)
// ---------------------------------------------------------------------------

///|
/// AgentGate 冲突判定(issue #25-A3 / #26-A3):
/// 1) 同一节点多条 tap flow → interaction_conflict
/// 2) 两个可点热区(被 tap flow 绑定的容器)包围盒相交且无父子包含 → hotzone_overlap
/// 任一命中即 Err("mbt_gate_block:<板>:<类>:<定位>"),由门路径拒绝。
pub fn qa_agent_conflicts(self : Project) -> Result[Unit, String] {
  for artboard_id in self.artboards.keys() {
    match self.artboards.get(artboard_id) {
      None => ()
      Some(ab) => {
        let tap_counts : Map[String, Int] = Map::new()
        for f in self.flows.iter() {
          if f.from == artboard_id && f.trigger.length() > 4 && f.trigger.has_prefix("tap:") {
            let nid = f.trigger.substring(start=4)
            let c = match tap_counts.get(nid) {
              Some(v) => v
              None => 0
            }
            tap_counts.set(nid, c + 1)
          }
        }
        for nid in tap_counts.keys() {
          match tap_counts.get(nid) {
            Some(c) =>
              if c > 1 {
                return Err(
                  "mbt_gate_block:" +
                  artboard_id +
                  ":interaction_conflict:" +
                  nid,
                )
              }
            None => ()
          }
        }
        let targets : Array[String] = []
        for k in tap_counts.keys() {
          targets.push(k)
        }
        let layout = solve(ab.doc)
        for i in 0..
                // 父子包含豁免(is_descendant 双向)+ 包含关系矩形豁免;
                // 两个可点区正相交即冲突(几何谓词复用 geom.mbt)
                if !(is_descendant(ab.doc, targets[i], targets[j]) ||
                is_descendant(ab.doc, targets[j], targets[i])) &&
                  !a.contains_rect(b) &&
                  !b.contains_rect(a) &&
                  a.intersects(b) {
                  return Err(
                    "mbt_gate_block:" +
                    artboard_id +
                    ":hotzone_overlap:" +
                    targets[i] +
                    ":" +
                    targets[j],
                  )
                }
              _ => ()
            }
          }
        }
      }
    }
  }
  Ok(())
}

///|
/// issue #25-B4(规则化):mock 数据缺口——只报告「哪个容器缺样例 / 哪段文本是
/// 占位符」,不做任何内容内置。真实样例内容由 Agent(LLM)按语义自行填写;
/// 引擎只保证规则可测(空壳 = 无子节点无文本;占位词表见 qa_is_placeholder_text)。
pub fn Project::mock_data_gaps(
  self : Project,
  artboard~ : String,
) -> Array[String] {
  let gaps : Array[String] = []
  match self.artboards.get(artboard) {
    None => ()
    Some(ab) =>
      for id in ab.doc.nodes.keys() {
        if id == ab.doc.root_id {
          continue
        }
        match ab.doc.nodes.get(id) {
          None => ()
          Some(n) => {
            if qa_is_placeholder_text(n.text) {
              gaps.push(id)
            } else if qa_is_mock_container(n.component) &&
              n.children.length() == 0 &&
              n.text.length() == 0 {
              gaps.push(id)
            }
          }
        }
      }
  }
  gaps
}