///|
pub(all) struct FixtureSpec {
  name : String
  description : String
  dsl : String
  expected : BtStatus
  max_ticks : Int
} derive(Debug, Eq)

///|
pub fn FixtureSpec::summary(self : FixtureSpec) -> String {
  self.name +
  ": expected=" +
  self.expected.to_text() +
  ", max_ticks=" +
  self.max_ticks.to_string() +
  ", description=" +
  self.description
}

///|
pub fn fixture_catalog() -> Array[FixtureSpec] {
  [
    guarded_attack_fixture(),
    patrol_fixture(),
    robot_delivery_fixture(),
    agent_tool_fixture(),
    boss_phase_fixture(),
    tutorial_hint_fixture(),
    stealth_guard_fixture(),
    harvest_worker_fixture(),
    qa_agent_fixture(),
    mission_selector_fixture(),
  ]
}

///|
pub fn fixture_names() -> Array[String] {
  let specs = fixture_catalog()
  let names = Array::new()
  let mut i = 0
  while i < specs.length() {
    names.push(specs[i].name)
    i = i + 1
  }
  names
}

///|
pub fn find_fixture(name : String) -> FixtureSpec? {
  let specs = fixture_catalog()
  let mut i = 0
  while i < specs.length() {
    if specs[i].name == name {
      return Some(specs[i])
    }
    i = i + 1
  }
  None
}

///|
pub fn load_fixture(name : String) -> Result[DslDocument, BtError] {
  match find_fixture(name) {
    Some(spec) => parse_dsl(spec.dsl)
    None => Err(ParseError("unknown fixture: " + name))
  }
}

///|
pub fn run_fixture(name : String) -> RunSummary {
  match find_fixture(name) {
    Some(spec) =>
      match parse_dsl(spec.dsl) {
        Ok(doc) =>
          smoke_run(spec.name, doc.tree, doc.blackboard, spec.expected, max_ticks=spec.max_ticks)
        Err(err) =>
          {
            name,
            final_status: Failure,
            ticks: 0,
            trace_events: 0,
            digest: "parse-error",
            ok: false,
            detail: err.message(),
          }
      }
    None =>
      {
        name,
        final_status: Failure,
        ticks: 0,
        trace_events: 0,
        digest: "missing-fixture",
        ok: false,
        detail: "unknown fixture",
      }
  }
}

///|
pub fn run_fixture_catalog() -> Array[RunSummary] {
  let specs = fixture_catalog()
  let out = Array::new()
  let mut i = 0
  while i < specs.length() {
    out.push(run_fixture(specs[i].name))
    i = i + 1
  }
  out
}

///|
pub fn fixture_report_lines() -> Array[String] {
  let summaries = run_fixture_catalog()
  let lines = Array::new()
  lines.push("MoonBTKit fixture report")
  let mut i = 0
  while i < summaries.length() {
    lines.push(summaries[i].to_line())
    i = i + 1
  }
  lines
}

///|
pub fn guarded_attack_fixture() -> FixtureSpec {
  {
    name: "guarded_attack",
    description: "NPC checks resources before aiming and firing at a visible enemy.",
    dsl: guarded_attack_dsl(),
    expected: Success,
    max_ticks: 6,
  }
}

///|
pub fn guarded_attack_dsl() -> String {
  "root root\n" +
  "blackboard enemy_visible true\n" +
  "blackboard ammo 3\n" +
  "blackboard stamina 8\n" +
  "selector root engage fallback\n" +
  "sequence engage see_enemy has_ammo has_stamina aim fire mark_done\n" +
  "condition see_enemy enemy_visible eq true\n" +
  "condition has_ammo ammo gt 0\n" +
  "condition has_stamina stamina ge 5\n" +
  "action aim aim_weapon running,success mode=aiming\n" +
  "action fire fire_weapon success ammo=2 last_action=fire\n" +
  "set mark_done combat_state engaged\n" +
  "sequence fallback emit_idle idle_wait\n" +
  "emit emit_idle idle\n" +
  "wait idle_wait 1\n"
}

///|
pub fn patrol_fixture() -> FixtureSpec {
  {
    name: "patrol",
    description: "Guard patrols when no enemy is visible and records the patrol route.",
    dsl: patrol_dsl(),
    expected: Success,
    max_ticks: 8,
  }
}

///|
pub fn patrol_dsl() -> String {
  "root root\n" +
  "blackboard enemy_visible false\n" +
  "blackboard waypoint 1\n" +
  "selector root engage patrol\n" +
  "sequence engage see_enemy attack\n" +
  "condition see_enemy enemy_visible eq true\n" +
  "action attack attack_target success state=attack\n" +
  "sequence patrol choose_route walk_a wait_a walk_b remember\n" +
  "emit choose_route route_selected\n" +
  "action walk_a walk_to_a running,success waypoint=2\n" +
  "wait wait_a 1\n" +
  "action walk_b walk_to_b running,success waypoint=3\n" +
  "set remember state patrolling\n"
}

///|
pub fn robot_delivery_fixture() -> FixtureSpec {
  {
    name: "robot_delivery",
    description: "Warehouse robot prefers charging before pickup when battery is low.",
    dsl: robot_delivery_dsl(),
    expected: Success,
    max_ticks: 12,
  }
}

///|
pub fn robot_delivery_dsl() -> String {
  "root root\n" +
  "blackboard battery 18\n" +
  "blackboard package_ready true\n" +
  "selector root charge_branch deliver_branch\n" +
  "sequence charge_branch low_battery dock charge undock\n" +
  "condition low_battery battery lt 25\n" +
  "action dock dock_to_station running,success docked=true\n" +
  "action charge charge_battery running,running,success battery=90\n" +
  "action undock leave_station success docked=false\n" +
  "sequence deliver_branch package_ready_node pickup navigate dropoff\n" +
  "condition package_ready_node package_ready eq true\n" +
  "action pickup pickup_package running,success carrying=true\n" +
  "action navigate navigate_to_target running,running,success location=target\n" +
  "action dropoff drop_package success carrying=false delivered=true\n"
}

///|
pub fn agent_tool_fixture() -> FixtureSpec {
  {
    name: "agent_tool",
    description: "AI agent chooses a safe search tool before a higher-risk shell action.",
    dsl: agent_tool_dsl(),
    expected: Success,
    max_ticks: 8,
  }
}

///|
pub fn agent_tool_dsl() -> String {
  "root root\n" +
  "blackboard needs_current_info true\n" +
  "blackboard shell_allowed false\n" +
  "selector root web_plan shell_plan fallback\n" +
  "sequence web_plan needs_info search summarize\n" +
  "condition needs_info needs_current_info eq true\n" +
  "action search web_search running,success evidence=found\n" +
  "action summarize summarize_evidence success answer=ready\n" +
  "sequence shell_plan can_shell run_shell\n" +
  "condition can_shell shell_allowed eq true\n" +
  "action run_shell shell_command success answer=ready\n" +
  "set fallback answer manual_review\n"
}

///|
pub fn boss_phase_fixture() -> FixtureSpec {
  {
    name: "boss_phase",
    description: "Game boss changes phase based on health and runs a telegraphed attack.",
    dsl: boss_phase_dsl(),
    expected: Success,
    max_ticks: 10,
  }
}

///|
pub fn boss_phase_dsl() -> String {
  "root root\n" +
  "blackboard hp 35\n" +
  "blackboard player_visible true\n" +
  "selector root phase_two phase_one recover\n" +
  "sequence phase_two low_hp visible roar charge slam\n" +
  "condition low_hp hp le 40\n" +
  "condition visible player_visible eq true\n" +
  "emit roar enraged\n" +
  "action charge charge_attack running,running,success attack=charged\n" +
  "action slam ground_slam running,success damage=heavy\n" +
  "sequence phase_one healthy basic_attack\n" +
  "condition healthy hp gt 40\n" +
  "action basic_attack basic_attack success damage=light\n" +
  "action recover recover_posture success state=recover\n"
}

///|
pub fn tutorial_hint_fixture() -> FixtureSpec {
  {
    name: "tutorial_hint",
    description: "Tutorial system waits, checks whether the player succeeded, then shows a hint.",
    dsl: tutorial_hint_dsl(),
    expected: Success,
    max_ticks: 6,
  }
}

///|
pub fn tutorial_hint_dsl() -> String {
  "root root\n" +
  "blackboard objective_done false\n" +
  "selector root already_done hint_path\n" +
  "sequence already_done done_cond celebrate\n" +
  "condition done_cond objective_done eq true\n" +
  "emit celebrate completed\n" +
  "sequence hint_path grace_wait still_not_done show_hint\n" +
  "wait grace_wait 2\n" +
  "inverter still_not_done done_cond\n" +
  "action show_hint show_context_hint success hint=shown\n"
}

///|
pub fn stealth_guard_fixture() -> FixtureSpec {
  {
    name: "stealth_guard",
    description: "Stealth guard escalates from suspicion to alarm after repeated sighting.",
    dsl: stealth_guard_dsl(),
    expected: Success,
    max_ticks: 10,
  }
}

///|
pub fn stealth_guard_dsl() -> String {
  "root root\n" +
  "blackboard noise_heard true\n" +
  "blackboard player_spotted true\n" +
  "selector root alarm investigate idle\n" +
  "sequence alarm spotted radio chase\n" +
  "condition spotted player_spotted eq true\n" +
  "action radio radio_alarm running,success alarm=true\n" +
  "action chase chase_player running,success state=chasing\n" +
  "sequence investigate heard turn search\n" +
  "condition heard noise_heard eq true\n" +
  "action turn turn_toward_noise success state=suspicious\n" +
  "action search search_area running,success state=searching\n" +
  "set idle state idle\n"
}

///|
pub fn harvest_worker_fixture() -> FixtureSpec {
  {
    name: "harvest_worker",
    description: "Simulation worker repeats harvest steps and stores resource progress.",
    dsl: harvest_worker_dsl(),
    expected: Success,
    max_ticks: 12,
  }
}

///|
pub fn harvest_worker_dsl() -> String {
  "root root\n" +
  "blackboard tool_ready true\n" +
  "sequence root ready repeat_cycle store\n" +
  "condition ready tool_ready eq true\n" +
  "repeat repeat_cycle 3 cycle\n" +
  "sequence cycle swing collect\n" +
  "action swing swing_tool running,success animation=swing\n" +
  "action collect collect_resource success resource=wood\n" +
  "set store task harvest_complete\n"
}

///|
pub fn qa_agent_fixture() -> FixtureSpec {
  {
    name: "qa_agent",
    description: "Agent triages whether to answer directly or ask for clarification.",
    dsl: qa_agent_dsl(),
    expected: Success,
    max_ticks: 5,
  }
}

///|
pub fn qa_agent_dsl() -> String {
  "root root\n" +
  "blackboard has_context true\n" +
  "blackboard ambiguity 1\n" +
  "selector root answer clarify\n" +
  "sequence answer context_ok ambiguity_low draft verify\n" +
  "condition context_ok has_context eq true\n" +
  "condition ambiguity_low ambiguity le 2\n" +
  "action draft draft_answer running,success draft=ready\n" +
  "action verify verify_answer success answer=ready\n" +
  "action clarify ask_clarification success answer=needs_input\n"
}

///|
pub fn mission_selector_fixture() -> FixtureSpec {
  {
    name: "mission_selector",
    description: "Quest controller picks emergency, main quest, or side quest by priority.",
    dsl: mission_selector_dsl(),
    expected: Success,
    max_ticks: 8,
  }
}

///|
pub fn mission_selector_dsl() -> String {
  "root root\n" +
  "blackboard emergency false\n" +
  "blackboard main_available true\n" +
  "blackboard reputation 7\n" +
  "selector root emergency_branch main_branch side_branch\n" +
  "sequence emergency_branch is_emergency evacuate\n" +
  "condition is_emergency emergency eq true\n" +
  "action evacuate start_evacuation success mission=emergency\n" +
  "sequence main_branch main_ok enough_rep start_main\n" +
  "condition main_ok main_available eq true\n" +
  "condition enough_rep reputation ge 5\n" +
  "action start_main start_main_quest running,success mission=main\n" +
  "action side_branch start_side_quest success mission=side\n"
}