#![allow(dead_code)]

//! Timeline screen state — event stream, scope filtering, navigation.
//!
//! The timeline displays a chronological event stream from resource event
//! tables. Events can be scoped to a specific entity, author, or shown
//! globally. [`TimelineScope`] gates the query; [`TimelineState`] manages
//! the scroll position, selected event, and generation counter for
//! stale-response detection.

use crate::message::{EntityKey, TimelineEvent};

// ---------------------------------------------------------------------------
// TimelineScope
// ---------------------------------------------------------------------------

/// Scope filter for the timeline event query.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TimelineScope {
    /// All events across all entities.
    #[default]
    All,
    /// Events for a specific entity (issue or MR).
    Entity(EntityKey),
    /// Events by a specific actor.
    Author(String),
}

// ---------------------------------------------------------------------------
// TimelineState
// ---------------------------------------------------------------------------

/// State for the timeline screen.
#[derive(Debug, Default)]
pub struct TimelineState {
    /// Loaded timeline events (sorted by timestamp, most recent first).
    pub events: Vec<TimelineEvent>,
    /// Active scope filter.
    pub scope: TimelineScope,
    /// Index of the selected event in the list.
    pub selected_index: usize,
    /// Scroll offset for the visible window.
    pub scroll_offset: usize,
    /// Monotonic generation counter for stale-response detection.
    pub generation: u64,
    /// Whether a fetch is in-flight.
    pub loading: bool,
}

impl TimelineState {
    /// Enter the timeline screen. Bumps generation for fresh data.
    pub fn enter(&mut self) -> u64 {
        self.generation += 1;
        self.loading = true;
        self.generation
    }

    /// Set the scope filter and bump generation.
    ///
    /// Returns the new generation (caller should trigger a re-fetch).
    pub fn set_scope(&mut self, scope: TimelineScope) -> u64 {
        self.scope = scope;
        self.generation += 1;
        self.loading = true;
        self.generation
    }

    /// Apply timeline events from an async response.
    ///
    /// Only applies if the generation matches (stale guard).
    pub fn apply_results(&mut self, generation: u64, events: Vec<TimelineEvent>) {
        if generation != self.generation {
            return; // Stale response — discard.
        }
        self.events = events;
        self.selected_index = 0;
        self.scroll_offset = 0;
        self.loading = false;
    }

    /// Move selection up in the event list.
    pub fn select_prev(&mut self) {
        self.selected_index = self.selected_index.saturating_sub(1);
    }

    /// Move selection down in the event list.
    pub fn select_next(&mut self) {
        if !self.events.is_empty() {
            self.selected_index = (self.selected_index + 1).min(self.events.len() - 1);
        }
    }

    /// Get the currently selected event, if any.
    #[must_use]
    pub fn selected_event(&self) -> Option<&TimelineEvent> {
        self.events.get(self.selected_index)
    }

    /// Ensure the selected index is visible given the viewport height.
    ///
    /// Adjusts `scroll_offset` so the selected item is within the
    /// visible window.
    pub fn ensure_visible(&mut self, viewport_height: usize) {
        if viewport_height == 0 {
            return;
        }
        if self.selected_index < self.scroll_offset {
            self.scroll_offset = self.selected_index;
        } else if self.selected_index >= self.scroll_offset + viewport_height {
            self.scroll_offset = self.selected_index - viewport_height + 1;
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::TimelineEventKind;

    fn sample_event(timestamp_ms: i64, iid: i64) -> TimelineEvent {
        TimelineEvent {
            timestamp_ms,
            entity_key: EntityKey::issue(1, iid),
            event_kind: TimelineEventKind::Created,
            summary: format!("Issue #{iid} created"),
            detail: None,
            actor: Some("alice".into()),
            project_path: "group/project".into(),
        }
    }

    #[test]
    fn test_timeline_scope_default_is_all() {
        assert_eq!(TimelineScope::default(), TimelineScope::All);
    }

    #[test]
    fn test_enter_bumps_generation() {
        let mut state = TimelineState::default();
        let generation = state.enter();
        assert_eq!(generation, 1);
        assert!(state.loading);
    }

    #[test]
    fn test_set_scope_bumps_generation() {
        let mut state = TimelineState::default();
        let gen1 = state.set_scope(TimelineScope::Author("bob".into()));
        assert_eq!(gen1, 1);
        assert_eq!(state.scope, TimelineScope::Author("bob".into()));

        let gen2 = state.set_scope(TimelineScope::All);
        assert_eq!(gen2, 2);
    }

    #[test]
    fn test_apply_results_matching_generation() {
        let mut state = TimelineState::default();
        let generation = state.enter();

        let events = vec![sample_event(3000, 1), sample_event(2000, 2)];
        state.apply_results(generation, events);

        assert_eq!(state.events.len(), 2);
        assert_eq!(state.selected_index, 0);
        assert!(!state.loading);
    }

    #[test]
    fn test_apply_results_stale_generation_discarded() {
        let mut state = TimelineState::default();
        state.enter(); // gen=1
        let _gen2 = state.enter(); // gen=2

        let stale_events = vec![sample_event(1000, 99)];
        state.apply_results(1, stale_events); // gen 1 is stale

        assert!(state.events.is_empty());
    }

    #[test]
    fn test_select_prev_next() {
        let mut state = TimelineState {
            events: vec![
                sample_event(3000, 1),
                sample_event(2000, 2),
                sample_event(1000, 3),
            ],
            ..TimelineState::default()
        };

        assert_eq!(state.selected_index, 0);
        state.select_next();
        assert_eq!(state.selected_index, 1);
        state.select_next();
        assert_eq!(state.selected_index, 2);
        state.select_next(); // Clamps at end.
        assert_eq!(state.selected_index, 2);
        state.select_prev();
        assert_eq!(state.selected_index, 1);
        state.select_prev();
        assert_eq!(state.selected_index, 0);
        state.select_prev(); // Clamps at start.
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn test_selected_event() {
        let mut state = TimelineState::default();
        assert!(state.selected_event().is_none());

        state.events = vec![sample_event(3000, 42)];
        let event = state.selected_event().unwrap();
        assert_eq!(event.entity_key.iid, 42);
    }

    #[test]
    fn test_ensure_visible_scrolls_down() {
        let mut state = TimelineState {
            events: vec![
                sample_event(5000, 1),
                sample_event(4000, 2),
                sample_event(3000, 3),
                sample_event(2000, 4),
                sample_event(1000, 5),
            ],
            selected_index: 4,
            scroll_offset: 0,
            ..TimelineState::default()
        };
        state.ensure_visible(3);
        assert_eq!(state.scroll_offset, 2); // 4 - 3 + 1 = 2
    }

    #[test]
    fn test_ensure_visible_scrolls_up() {
        let mut state = TimelineState {
            events: vec![
                sample_event(5000, 1),
                sample_event(4000, 2),
                sample_event(3000, 3),
            ],
            selected_index: 0,
            scroll_offset: 2,
            ..TimelineState::default()
        };
        state.ensure_visible(3);
        assert_eq!(state.scroll_offset, 0);
    }

    #[test]
    fn test_ensure_visible_zero_viewport() {
        let mut state = TimelineState {
            scroll_offset: 5,
            ..TimelineState::default()
        };
        state.ensure_visible(0);
        assert_eq!(state.scroll_offset, 5); // Unchanged.
    }

    #[test]
    fn test_select_next_on_empty_is_noop() {
        let mut state = TimelineState::default();
        state.select_next();
        assert_eq!(state.selected_index, 0);
    }
}
