// ---------------------------------------------------------------------------
// RecordingObserver — preserves full event order.
// ---------------------------------------------------------------------------

///|
/// Observer fake that records every TurnEvent in arrival order.
/// Unlike a counter, it keeps the full sequence for trace assertions.
pub(all) struct RecordingObserver {
  mut events : Array[@types.TurnEvent]
}

///|
pub fn RecordingObserver::RecordingObserver() -> RecordingObserver {
  { events: [], }
}

///|
/// Snapshot of all recorded events in order.
pub fn RecordingObserver::events(
  self : RecordingObserver,
) -> Array[@types.TurnEvent] {
  self.events.map(testkit_snapshot_event)
}

///|
pub impl @port.Observer for RecordingObserver with fn on_event(self, event) {
  self.events.push(testkit_snapshot_event(event))
}

// ---------------------------------------------------------------------------
// RecordingHook — records hook invocations (native hook traits).
// ---------------------------------------------------------------------------

///|
/// The exact decision made by a RecordingHook invocation.
pub(all) enum RecordedHookOutcome {
  PassedThrough
  AbortedWith(String)
  DeferredWith(String)
} derive(Debug)

///|
/// A recorded hook invocation: which hook function ran and its decision.
pub(all) struct HookRecord {
  label : String
  outcome : RecordedHookOutcome
} derive(Debug)

///|
/// Multi-hook fake that implements `PipelineHook` at all three interception points.
/// By default it is a pass-through (returns messages unchanged, approves
/// tool calls). Records every call.
pub(all) struct RecordingHook {
  mut records : Array[HookRecord]
  abort_on : Map[String, String]
  defer_on : Map[String, String]
}

///|
pub fn RecordingHook::RecordingHook() -> RecordingHook {
  { records: [], abort_on: Map::from_array([]), defer_on: Map::from_array([]), }
}

///|
/// Configure the hook to abort/reject when the label contains `trigger`.
pub fn RecordingHook::abort_when(
  self : RecordingHook,
  trigger : String,
  msg : String,
) -> Unit {
  self.abort_on[trigger] = msg
}

///|
/// Configure the hook to defer when the label contains `trigger`.
pub fn RecordingHook::defer_when(
  self : RecordingHook,
  trigger : String,
  reason : String,
) -> Unit {
  self.defer_on[trigger] = reason
}

///|
pub fn RecordingHook::records(self : RecordingHook) -> Array[HookRecord] {
  self.records.map(fn(record) {
    { label: record.label, outcome: record.outcome, }
  })
}

///|
pub impl @port.PipelineHook for RecordingHook with fn before_model(
  self,
  messages : Array[@kernel.Message],
) -> Array[@kernel.Message] raise @port.HookAbort {
  let label = "before_model"
  for trigger in self.abort_on.keys() {
    if label.contains(trigger) ||
      trigger.contains("BeforeModel") ||
      trigger.contains("BeforeTurn") {
      let message = self.abort_on[trigger]
      self.records.push({ label, outcome: AbortedWith(message), })
      raise @port.HookAbort::Aborted(reason=message)
    }
  }
  self.records.push({ label, outcome: PassedThrough, })
  messages
}

///|
pub impl @port.PipelineHook for RecordingHook with fn before_tool(
  self,
  call : @kernel.ToolCall,
) -> @port.ToolDecision {
  let label = "before_tool"
  let dbg = call.call_id.to_string()
  for trigger in self.abort_on.keys() {
    if dbg.contains(trigger) {
      let message = self.abort_on[trigger]
      self.records.push({ label, outcome: AbortedWith(message), })
      return Reject(reason=message)
    }
  }
  for trigger in self.defer_on.keys() {
    if dbg.contains(trigger) {
      let reason = self.defer_on[trigger]
      self.records.push({ label, outcome: DeferredWith(reason), })
      return Defer(reason~)
    }
  }
  self.records.push({ label, outcome: PassedThrough, })
  Approve(call~)
}

///|
pub impl @port.PipelineHook for RecordingHook with fn on_post_event(
  self,
  _stage : @port.HookStage,
) -> Unit {
  let label = "on_post_event"
  self.records.push({ label, outcome: PassedThrough, })
}