///|
/// `RecordingUiPort` — testkit UiPort that records every `render` and replies
/// to `request` from a scripted queue.
///
/// Use this in tests to:
/// - assert extensions emitted the expected render sequence;
/// - script request responses (Input/Confirm/Select) without a real UI;
/// - verify that downstream code reacts correctly to `Unsupported`/`Cancelled`.
///
/// The struct is intentionally not `pub(all)`-field-exposing: tests inspect
/// state through methods (`rendered_intents`, `next_request_response`, etc.)
/// so the recording layout can evolve without breaking tests.

///|
pub(all) struct RecordingUiPort {
  mut rendered : Array[@port.UiRender]
  /// Scripted responses, drained in FIFO order. Once empty, the next
  /// `request` raises `Unsupported` so tests fail loudly on unexpected
  /// interaction rather than hanging.
  mut scripted_responses : Array[@port.UiResponse]
  /// Whether `request` should raise `Unsupported` regardless of the script.
  /// Used to test the "no UI available" path. Defaults to false.
  mut unsupported_mode : Bool
  /// Descriptor returned by `ui_descriptor`. Defaults to empty; tests that
  /// need to verify descriptor aggregation set this explicitly.
  mut descriptor : @port.UiDescriptor
}

///|
/// Construct with a list of scripted responses. They will be returned in
/// order, one per `request` call.
pub fn RecordingUiPort::RecordingUiPort(
  responses? : Array[@port.UiResponse] = [],
) -> RecordingUiPort {
  {
    rendered: [],
    scripted_responses: responses.copy(),
    unsupported_mode: false,
    descriptor: @port.UiDescriptor::empty(),
  }
}

///|
/// Construct a UiPort that always raises `Unsupported` from `request`.
pub fn RecordingUiPort::new_unsupported() -> RecordingUiPort {
  let r = RecordingUiPort()
  r.unsupported_mode = true
  r
}

///|
/// All render intents received, in arrival order. The returned array is a
/// copy; mutating it does not affect the recording.
pub fn RecordingUiPort::rendered_intents(
  self : RecordingUiPort,
) -> Array[@port.UiRender] {
  self.rendered.copy()
}

///|
/// Number of render calls received.
pub fn RecordingUiPort::render_count(self : RecordingUiPort) -> Int {
  self.rendered.length()
}

///|
/// Filter rendered intents by slot. Useful for asserting "exactly one Notice
/// was emitted" without caring about other slots.
pub fn RecordingUiPort::rendered_in_slot(
  self : RecordingUiPort,
  slot : @port.UiSlot,
) -> Array[@port.UiRender] {
  let out : Array[@port.UiRender] = []
  for r in self.rendered {
    match (r.slot, slot) {
      (@port.Status, @port.Status)
      | (@port.Notice, @port.Notice)
      | (@port.Widget, @port.Widget) => out.push(r)
      _ => ()
    }
  }
  out
}

///|
/// Number of `request` calls that have NOT yet been answered (i.e. consumed
/// a scripted response). Useful to assert that no extra requests happened.
pub fn RecordingUiPort::unconsumed_response_count(
  self : RecordingUiPort,
) -> Int {
  self.scripted_responses.length()
}

///|
/// Set the descriptor returned by `ui_descriptor`.
pub fn RecordingUiPort::set_descriptor(
  self : RecordingUiPort,
  descriptor : @port.UiDescriptor,
) -> Unit {
  self.descriptor = descriptor
}

///|
pub impl @port.UiPort for RecordingUiPort with fn ui_descriptor(self) -> @port.UiDescriptor {
  self.descriptor
}

///|
pub impl @port.UiPort for RecordingUiPort with fn render(
  self,
  intent : @port.UiRender,
) -> Unit {
  self.rendered.push(intent)
}

///|
pub impl @port.UiPort for RecordingUiPort with fn request(
  self,
  _req : @port.UiRequest,
) -> @port.UiResponse {
  if self.unsupported_mode {
    raise @error.UiError::Unsupported(
      detail="RecordingUiPort is in unsupported_mode",
    )
  }
  // FIFO: drain from the front. Array::remove panics on out-of-bounds, so we
  // guard with an explicit length check and raise Unsupported (not a panic)
  // when the script is empty — that way CompositeUiPort can fall through and
  // tests see a typed error instead of a runtime crash.
  if self.scripted_responses.length() == 0 {
    raise @error.UiError::Unsupported(
      detail="RecordingUiPort script exhausted; no response for request",
    )
  }
  self.scripted_responses.remove(0)
}

///|
/// Expose UiPort methods on RecordingUiPort for dot-syntax callers.
pub extend RecordingUiPort with @port.UiPort::{ui_descriptor, render, request}