#![allow(dead_code)] // Phase 1: consumed by LoreApp in bd-6pmy

//! Browser-like navigation stack with vim-style jump list.
//!
//! Supports back/forward (browser), jump back/forward (vim Ctrl+O/Ctrl+I),
//! and breadcrumb generation. State is preserved when navigating away —
//! screens are never cleared on pop.

use crate::message::Screen;

// ---------------------------------------------------------------------------
// NavigationStack
// ---------------------------------------------------------------------------

/// Browser-like navigation with back/forward stacks and a vim jump list.
///
/// The jump list only records "significant" hops — detail views and
/// cross-references — skipping list/dashboard screens that users
/// visit briefly during drilling.
pub struct NavigationStack {
    back_stack: Vec<Screen>,
    current: Screen,
    forward_stack: Vec<Screen>,
    jump_list: Vec<Screen>,
    jump_index: usize,
}

impl NavigationStack {
    /// Create a new stack starting at the Dashboard.
    #[must_use]
    pub fn new() -> Self {
        Self {
            back_stack: Vec::new(),
            current: Screen::Dashboard,
            forward_stack: Vec::new(),
            jump_list: Vec::new(),
            jump_index: 0,
        }
    }

    /// The currently displayed screen.
    #[must_use]
    pub fn current(&self) -> &Screen {
        &self.current
    }

    /// Whether the current screen matches the given screen.
    #[must_use]
    pub fn is_at(&self, screen: &Screen) -> bool {
        &self.current == screen
    }

    /// Navigate to a new screen.
    ///
    /// Pushes current to back_stack, clears forward_stack (browser behavior),
    /// and records detail hops in the jump list.
    pub fn push(&mut self, screen: Screen) {
        let old = std::mem::replace(&mut self.current, screen);
        self.back_stack.push(old);
        self.forward_stack.clear();

        // Record significant hops in jump list (vim behavior):
        // Keep entries up to and including the current position, discard
        // any forward entries beyond it, then append the new destination.
        if self.current.is_detail_or_entity() {
            self.jump_list.truncate(self.jump_index.saturating_add(1));
            self.jump_list.push(self.current.clone());
            self.jump_index = self.jump_list.len();
        }
    }

    /// Go back to the previous screen.
    ///
    /// Returns `None` at root (can't pop past the initial screen).
    pub fn pop(&mut self) -> Option<&Screen> {
        let prev = self.back_stack.pop()?;
        let old = std::mem::replace(&mut self.current, prev);
        self.forward_stack.push(old);
        Some(&self.current)
    }

    /// Go forward (redo a pop).
    ///
    /// Returns `None` if there's nothing to go forward to.
    pub fn go_forward(&mut self) -> Option<&Screen> {
        let next = self.forward_stack.pop()?;
        let old = std::mem::replace(&mut self.current, next);
        self.back_stack.push(old);
        Some(&self.current)
    }

    /// Jump backward through the jump list (vim Ctrl+O).
    ///
    /// Only visits detail/entity screens. Skips entries matching the
    /// current screen so the first press always produces a visible change.
    pub fn jump_back(&mut self) -> Option<&Screen> {
        while self.jump_index > 0 {
            self.jump_index -= 1;
            if let Some(target) = self.jump_list.get(self.jump_index).cloned()
                && target != self.current
            {
                self.current = target;
                return Some(&self.current);
            }
        }
        None
    }

    /// Jump forward through the jump list (vim Ctrl+I).
    ///
    /// Skips entries matching the current screen.
    pub fn jump_forward(&mut self) -> Option<&Screen> {
        while self.jump_index < self.jump_list.len() {
            if let Some(target) = self.jump_list.get(self.jump_index).cloned() {
                self.jump_index += 1;
                if target != self.current {
                    self.current = target;
                    return Some(&self.current);
                }
            } else {
                break;
            }
        }
        None
    }

    /// Reset to a single screen, clearing all history.
    pub fn reset_to(&mut self, screen: Screen) {
        self.current = screen;
        self.back_stack.clear();
        self.forward_stack.clear();
        self.jump_list.clear();
        self.jump_index = 0;
    }

    /// Breadcrumb labels for the current navigation path.
    ///
    /// Returns the back stack labels plus the current screen label.
    #[must_use]
    pub fn breadcrumbs(&self) -> Vec<&str> {
        self.back_stack
            .iter()
            .chain(std::iter::once(&self.current))
            .map(Screen::label)
            .collect()
    }

    /// Navigation depth (1 = at root, 2 = one push deep, etc.).
    #[must_use]
    pub fn depth(&self) -> usize {
        self.back_stack.len() + 1
    }

    /// Whether there's anything to go back to.
    #[must_use]
    pub fn can_go_back(&self) -> bool {
        !self.back_stack.is_empty()
    }

    /// Whether there's anything to go forward to.
    #[must_use]
    pub fn can_go_forward(&self) -> bool {
        !self.forward_stack.is_empty()
    }
}

impl Default for NavigationStack {
    fn default() -> Self {
        Self::new()
    }
}

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

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

    #[test]
    fn test_new_starts_at_dashboard() {
        let nav = NavigationStack::new();
        assert!(nav.is_at(&Screen::Dashboard));
        assert_eq!(nav.depth(), 1);
    }

    #[test]
    fn test_push_pop_preserves_order() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueList);
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 42)));

        assert!(nav.is_at(&Screen::IssueDetail(EntityKey::issue(1, 42))));
        assert_eq!(nav.depth(), 3);

        nav.pop();
        assert!(nav.is_at(&Screen::IssueList));

        nav.pop();
        assert!(nav.is_at(&Screen::Dashboard));
    }

    #[test]
    fn test_pop_at_root_returns_none() {
        let mut nav = NavigationStack::new();
        assert!(nav.pop().is_none());
        assert!(nav.is_at(&Screen::Dashboard));
    }

    #[test]
    fn test_forward_stack_cleared_on_new_push() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueList);
        nav.push(Screen::Search);
        nav.pop(); // back to IssueList, Search in forward
        assert!(nav.can_go_forward());

        nav.push(Screen::Timeline); // new push clears forward
        assert!(!nav.can_go_forward());
    }

    #[test]
    fn test_go_forward_restores() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueList);
        nav.push(Screen::Search);
        nav.pop(); // back to IssueList

        let screen = nav.go_forward();
        assert!(screen.is_some());
        assert!(nav.is_at(&Screen::Search));
    }

    #[test]
    fn test_go_forward_returns_none_when_empty() {
        let mut nav = NavigationStack::new();
        assert!(nav.go_forward().is_none());
    }

    #[test]
    fn test_jump_list_skips_list_screens() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueList); // not a detail — skip
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 1))); // detail — record
        nav.push(Screen::MrList); // not a detail — skip
        nav.push(Screen::MrDetail(EntityKey::mr(1, 2))); // detail — record

        assert_eq!(nav.jump_list.len(), 2);
    }

    #[test]
    fn test_jump_back_and_forward() {
        let mut nav = NavigationStack::new();
        let issue = Screen::IssueDetail(EntityKey::issue(1, 1));
        let mr = Screen::MrDetail(EntityKey::mr(1, 2));

        nav.push(Screen::IssueList);
        nav.push(issue.clone());
        nav.push(Screen::MrList);
        nav.push(mr.clone());

        // Current is MrDetail. jump_list = [IssueDetail, MrDetail], index = 2.
        // First jump_back skips MrDetail (== current) and lands on IssueDetail.
        let prev = nav.jump_back();
        assert_eq!(prev, Some(&issue));
        assert!(nav.is_at(&issue));

        // Already at beginning of jump list.
        assert!(nav.jump_back().is_none());

        // jump_forward skips IssueDetail (== current) and lands on MrDetail.
        let next = nav.jump_forward();
        assert_eq!(next, Some(&mr));
        assert!(nav.is_at(&mr));

        // At end of jump list.
        assert!(nav.jump_forward().is_none());
    }

    #[test]
    fn test_jump_list_truncates_on_new_push() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 1)));
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 2)));
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 3)));

        // jump back twice — lands on issue(1,1), jump_index = 0
        nav.jump_back();
        nav.jump_back();

        // new detail push truncates forward entries
        nav.push(Screen::MrDetail(EntityKey::mr(1, 99)));

        // should have issue(1,1) and mr(1,99), not issue(1,2) or issue(1,3)
        assert_eq!(nav.jump_list.len(), 2);
        assert_eq!(nav.jump_list[1], Screen::MrDetail(EntityKey::mr(1, 99)));
    }

    #[test]
    fn test_reset_clears_all_history() {
        let mut nav = NavigationStack::new();
        nav.push(Screen::IssueList);
        nav.push(Screen::Search);
        nav.push(Screen::IssueDetail(EntityKey::issue(1, 1)));

        nav.reset_to(Screen::Dashboard);

        assert!(nav.is_at(&Screen::Dashboard));
        assert_eq!(nav.depth(), 1);
        assert!(!nav.can_go_back());
        assert!(!nav.can_go_forward());
        assert!(nav.jump_list.is_empty());
    }

    #[test]
    fn test_breadcrumbs_reflect_stack() {
        let mut nav = NavigationStack::new();
        assert_eq!(nav.breadcrumbs(), vec!["Dashboard"]);

        nav.push(Screen::IssueList);
        assert_eq!(nav.breadcrumbs(), vec!["Dashboard", "Issues"]);

        nav.push(Screen::IssueDetail(EntityKey::issue(1, 42)));
        assert_eq!(nav.breadcrumbs(), vec!["Dashboard", "Issues", "Issue"]);
    }

    #[test]
    fn test_default_is_new() {
        let nav = NavigationStack::default();
        assert!(nav.is_at(&Screen::Dashboard));
        assert_eq!(nav.depth(), 1);
    }

    #[test]
    fn test_can_go_back_and_forward() {
        let mut nav = NavigationStack::new();
        assert!(!nav.can_go_back());
        assert!(!nav.can_go_forward());

        nav.push(Screen::IssueList);
        assert!(nav.can_go_back());
        assert!(!nav.can_go_forward());

        nav.pop();
        assert!(!nav.can_go_back());
        assert!(nav.can_go_forward());
    }
}
