//! [`StatusEventBuffer`] — bounded, per-instance accumulator over a
//! [`StatusBroadcaster`].
#![allow(dead_code)]
//!
//! A `StatusEventBuffer` subscribes to a [`StatusBroadcaster`] on construction
//! and silently accumulates incoming [`StatusEvent`]s into a bounded
//! [`VecDeque`].  Callers retrieve events later via [`StatusEventBuffer::drain`]
//! or discard them with [`StatusEventBuffer::clear`].
//!
//! # Capacity and overflow
//!
//! The buffer has a configurable capacity (default [`DEFAULT_CAPACITY`]).  When
//! a new event would exceed the limit, the **oldest** event is evicted and a
//! truncation counter is incremented.  The next call to
//! [`StatusEventBuffer::drain`] prepends a [`BufferedItem::Truncated`] marker
//! so that callers know events were lost.
//!
//! # No side effects on arrival
//!
//! Incoming events only update the internal [`VecDeque`]; no callbacks, model
//! calls, or other logic are invoked.  The background receive task is spawned
//! on construction and aborted when the buffer is dropped.
//!
//! # Two-buffer independence
//!
//! Each buffer holds its own [`Subscription`] handle, so two buffers
//! constructed over the same broadcaster both receive every event
//! independently.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};

use super::{StatusBroadcaster, StatusEvent};

/// Default maximum number of events the buffer retains before evicting the oldest.
pub const DEFAULT_CAPACITY: usize = 100;

// ── BufferedItem ──────────────────────────────────────────────────────────────

/// An item returned by [`StatusEventBuffer::drain`].
#[derive(Debug)]
pub enum BufferedItem {
    /// One or more older events were evicted to make room; `N` is the total
    /// count of evicted events since the last [`drain`](StatusEventBuffer::drain).
    Truncated(usize),
    /// A pipeline status event accumulated by the buffer.
    Event(StatusEvent),
}

// ── Inner ─────────────────────────────────────────────────────────────────────

struct Inner {
    events: VecDeque<StatusEvent>,
    capacity: usize,
    /// Number of events evicted since the last `drain`.
    truncated: usize,
}

impl Inner {
    fn new(capacity: usize) -> Self {
        Self {
            events: VecDeque::new(),
            capacity,
            truncated: 0,
        }
    }

    fn push(&mut self, event: StatusEvent) {
        if self.events.len() >= self.capacity {
            self.events.pop_front();
            self.truncated += 1;
        }
        self.events.push_back(event);
    }

    fn drain(&mut self) -> Vec<BufferedItem> {
        let extra = if self.truncated > 0 { 1 } else { 0 };
        let mut out = Vec::with_capacity(self.events.len() + extra);
        if self.truncated > 0 {
            out.push(BufferedItem::Truncated(self.truncated));
            self.truncated = 0;
        }
        out.extend(self.events.drain(..).map(BufferedItem::Event));
        out
    }

    fn clear(&mut self) {
        self.events.clear();
        self.truncated = 0;
    }
}

// ── StatusEventBuffer ─────────────────────────────────────────────────────────

/// A bounded, per-instance accumulator that subscribes to a
/// [`StatusBroadcaster`] and stores incoming events without triggering any
/// side effects.
///
/// Construct with [`StatusEventBuffer::new`] (default capacity) or
/// [`StatusEventBuffer::with_capacity`].  Retrieve accumulated events with
/// [`drain`](StatusEventBuffer::drain) or discard them with
/// [`clear`](StatusEventBuffer::clear).
pub struct StatusEventBuffer {
    inner: Arc<Mutex<Inner>>,
    // Held only to abort the background receive task on drop.
    _task: tokio::task::JoinHandle<()>,
}

impl StatusEventBuffer {
    /// Create a buffer with [`DEFAULT_CAPACITY`] subscribed to `broadcaster`.
    pub fn new(broadcaster: &StatusBroadcaster) -> Self {
        Self::with_capacity(broadcaster, DEFAULT_CAPACITY)
    }

    /// Create a buffer with a custom `capacity` subscribed to `broadcaster`.
    ///
    /// When the buffer is full and a new event arrives, the oldest stored event
    /// is dropped and the truncation counter is incremented.
    pub fn with_capacity(broadcaster: &StatusBroadcaster, capacity: usize) -> Self {
        let inner = Arc::new(Mutex::new(Inner::new(capacity)));
        let inner_clone = Arc::clone(&inner);
        let mut sub = broadcaster.subscribe();

        let task = tokio::spawn(async move {
            while let Some(event) = sub.recv().await {
                let mut guard = inner_clone
                    .lock()
                    .expect("StatusEventBuffer mutex poisoned");
                guard.push(event);
            }
        });

        Self { inner, _task: task }
    }

    /// Return and remove all accumulated events in arrival order.
    ///
    /// If any events were evicted due to overflow since the last call, the
    /// returned `Vec` is **prepended** with a [`BufferedItem::Truncated`] entry
    /// indicating how many events were lost.
    pub fn drain(&self) -> Vec<BufferedItem> {
        self.inner
            .lock()
            .expect("StatusEventBuffer mutex poisoned")
            .drain()
    }

    /// Discard all accumulated events and reset the truncation counter.
    pub fn clear(&self) {
        self.inner
            .lock()
            .expect("StatusEventBuffer mutex poisoned")
            .clear();
    }
}

impl Drop for StatusEventBuffer {
    fn drop(&mut self) {
        self._task.abort();
    }
}

// ── Formatting ────────────────────────────────────────────────────────────────

/// Format a [`BufferedItem`] slice into a `<recent-events>…</recent-events>` block
/// ready to prepend to an agent message.
///
/// Each [`BufferedItem::Event`] is rendered with
/// [`format_status_event`](crate::service::status::format::format_status_event).
/// A [`BufferedItem::Truncated`] entry produces a note about how many older
/// events were omitted.
///
/// Returns `None` when `items` is empty so callers can skip the prepend
/// entirely.
pub fn format_buffered_items(items: &[BufferedItem]) -> Option<String> {
    if items.is_empty() {
        return None;
    }
    use crate::service::status::format::format_status_event;
    let mut lines = Vec::with_capacity(items.len());
    for item in items {
        match item {
            BufferedItem::Truncated(n) => {
                lines.push(format!("[{n} older event(s) were omitted due to overflow]"));
            }
            BufferedItem::Event(event) => {
                lines.push(format_status_event(event));
            }
        }
    }
    Some(format!(
        "<recent-events>\n{}\n</recent-events>",
        lines.join("\n")
    ))
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::time::{Duration, sleep};

    fn make_event(id: &str) -> StatusEvent {
        StatusEvent::MergeFailure {
            story_id: id.to_string(),
            story_name: String::new(),
            reason: "test".to_string(),
        }
    }

    /// Helper: publish `n` events and yield so the background task can process them.
    async fn publish_n(broadcaster: &StatusBroadcaster, n: usize) {
        for i in 0..n {
            broadcaster.publish(make_event(&format!("story_{i}")));
        }
        // Yield to let the background receive task process the events.
        sleep(Duration::from_millis(20)).await;
    }

    // ── arrival accumulation ──────────────────────────────────────────────────

    #[tokio::test]
    async fn events_accumulate_in_order() {
        let bc = StatusBroadcaster::new();
        let buf = StatusEventBuffer::new(&bc);

        publish_n(&bc, 3).await;

        let items = buf.drain();
        assert_eq!(items.len(), 3);
        for (i, item) in items.iter().enumerate() {
            match item {
                BufferedItem::Event(StatusEvent::MergeFailure { story_id, .. }) => {
                    assert_eq!(story_id, &format!("story_{i}"));
                }
                other => panic!("unexpected item: {other:?}"),
            }
        }
    }

    // ── drain returns events then clears ──────────────────────────────────────

    #[tokio::test]
    async fn drain_clears_the_buffer() {
        let bc = StatusBroadcaster::new();
        let buf = StatusEventBuffer::new(&bc);

        publish_n(&bc, 2).await;

        let first = buf.drain();
        assert_eq!(first.len(), 2);

        // Second drain on an empty buffer returns nothing.
        let second = buf.drain();
        assert!(second.is_empty(), "drain should clear the buffer");
    }

    // ── overflow truncation marker ────────────────────────────────────────────

    #[tokio::test]
    async fn overflow_prepends_truncation_marker() {
        let bc = StatusBroadcaster::new();
        // Capacity 3: publishing 5 events should evict 2.
        let buf = StatusEventBuffer::with_capacity(&bc, 3);

        publish_n(&bc, 5).await;

        let items = buf.drain();
        // First item must be a Truncated(2) marker.
        match items.first() {
            Some(BufferedItem::Truncated(n)) => assert_eq!(*n, 2, "should report 2 truncated"),
            other => panic!("expected Truncated, got {other:?}"),
        }
        // Remaining 3 items are the last 3 events (story_2, story_3, story_4).
        assert_eq!(items.len(), 4, "truncation marker + 3 events");
        match &items[1] {
            BufferedItem::Event(StatusEvent::MergeFailure { story_id, .. }) => {
                assert_eq!(story_id, "story_2");
            }
            other => panic!("unexpected: {other:?}"),
        }
    }

    #[tokio::test]
    async fn truncation_counter_resets_after_drain() {
        let bc = StatusBroadcaster::new();
        let buf = StatusEventBuffer::with_capacity(&bc, 2);

        // Overflow: publish 4 events into a capacity-2 buffer.
        publish_n(&bc, 4).await;

        let first_drain = buf.drain();
        assert!(
            matches!(first_drain[0], BufferedItem::Truncated(_)),
            "first drain should have truncation marker"
        );

        // Publish 1 more event — no overflow this time.
        publish_n(&bc, 1).await;

        let second_drain = buf.drain();
        assert_eq!(second_drain.len(), 1, "one event, no truncation marker");
        assert!(matches!(second_drain[0], BufferedItem::Event(_)));
    }

    // ── no-trigger invariant ──────────────────────────────────────────────────

    /// Events accumulate in the buffer without invoking any side effect.
    /// We verify this by checking that `drain()` returns exactly the events
    /// published and no external state (represented by a counter) was modified.
    #[tokio::test]
    async fn event_arrival_has_no_side_effects_beyond_accumulation() {
        let bc = StatusBroadcaster::new();
        let buf = StatusEventBuffer::new(&bc);

        // A shared flag that should NEVER be set by buffer internals.
        let side_effect_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));

        // Publish 10 events — buffer accumulates silently.
        publish_n(&bc, 10).await;

        // The flag was never touched.
        assert!(
            !side_effect_flag.load(std::sync::atomic::Ordering::Relaxed),
            "no side effects should have occurred"
        );

        // All 10 events accumulated.
        let items = buf.drain();
        assert_eq!(items.len(), 10);
        for item in &items {
            assert!(
                matches!(item, BufferedItem::Event(_)),
                "all items should be events"
            );
        }
    }

    // ── two-buffer independence ───────────────────────────────────────────────

    /// Two buffers on the same broadcaster each receive the same events independently.
    #[tokio::test]
    async fn two_buffers_on_same_broadcaster_see_same_events() {
        let bc = StatusBroadcaster::new();
        let buf_a = StatusEventBuffer::new(&bc);
        let buf_b = StatusEventBuffer::new(&bc);

        publish_n(&bc, 4).await;

        let items_a = buf_a.drain();
        let items_b = buf_b.drain();

        assert_eq!(items_a.len(), 4, "buf_a should see 4 events");
        assert_eq!(items_b.len(), 4, "buf_b should see 4 events");

        // Both buffers see events in the same order.
        for (a, b) in items_a.iter().zip(items_b.iter()) {
            match (a, b) {
                (
                    BufferedItem::Event(StatusEvent::MergeFailure { story_id: ia, .. }),
                    BufferedItem::Event(StatusEvent::MergeFailure { story_id: ib, .. }),
                ) => assert_eq!(ia, ib, "both buffers should see events in the same order"),
                _ => panic!("unexpected item types"),
            }
        }
    }

    // ── clear ─────────────────────────────────────────────────────────────────

    #[tokio::test]
    async fn clear_discards_accumulated_events_and_truncation_count() {
        let bc = StatusBroadcaster::new();
        let buf = StatusEventBuffer::with_capacity(&bc, 2);

        // Overflow to set truncation counter.
        publish_n(&bc, 4).await;

        buf.clear();

        // After clear, drain should return nothing.
        let items = buf.drain();
        assert!(
            items.is_empty(),
            "clear should discard events and truncation marker"
        );
    }

    // ── format_buffered_items ─────────────────────────────────────────────────

    #[test]
    fn format_empty_slice_returns_none() {
        assert!(format_buffered_items(&[]).is_none());
    }

    #[test]
    fn format_events_produces_recent_events_block() {
        let items = vec![
            BufferedItem::Event(StatusEvent::MergeFailure {
                story_id: "42_story_foo".to_string(),
                story_name: "Foo".to_string(),
                reason: "conflict".to_string(),
            }),
            BufferedItem::Event(StatusEvent::StoryBlocked {
                story_id: "7_story_bar".to_string(),
                story_name: String::new(),
                reason: "retry limit".to_string(),
            }),
        ];
        let block = format_buffered_items(&items).expect("non-empty slice must produce a block");
        assert!(
            block.starts_with("<recent-events>\n"),
            "must open with <recent-events>: {block}"
        );
        assert!(
            block.ends_with("\n</recent-events>"),
            "must close with </recent-events>: {block}"
        );
        assert!(block.contains("conflict"), "must include event content");
        assert!(block.contains("retry limit"), "must include event content");
    }

    #[test]
    fn format_truncation_marker_is_included() {
        let items = vec![
            BufferedItem::Truncated(3),
            BufferedItem::Event(StatusEvent::MergeFailure {
                story_id: "1_story_x".to_string(),
                story_name: String::new(),
                reason: "test".to_string(),
            }),
        ];
        let block = format_buffered_items(&items).unwrap();
        assert!(
            block.contains("3 older event(s) were omitted"),
            "truncation note must appear in block: {block}"
        );
    }
}
