#![allow(dead_code)]

//! Search screen state — query input, mode selection, capability detection.
//!
//! The search screen supports three modes ([`SearchMode`]): Lexical (FTS5),
//! Hybrid (FTS+vector RRF), and Semantic (vector-only). Available modes are
//! gated by [`SearchCapabilities`], which probes the database on screen entry.

use crate::message::{SearchMode, SearchResult};

// ---------------------------------------------------------------------------
// SearchCapabilities
// ---------------------------------------------------------------------------

/// What search indexes are available in the local database.
///
/// Detected once on screen entry by probing FTS and embedding tables.
/// Used to gate which [`SearchMode`] values are selectable.
#[derive(Debug, Clone, PartialEq)]
pub struct SearchCapabilities {
    /// FTS5 `documents_fts` table has rows.
    pub has_fts: bool,
    /// `embedding_metadata` table has rows.
    pub has_embeddings: bool,
    /// Percentage of documents that have embeddings (0.0–100.0).
    pub embedding_coverage_pct: f32,
}

impl Default for SearchCapabilities {
    fn default() -> Self {
        Self {
            has_fts: false,
            has_embeddings: false,
            embedding_coverage_pct: 0.0,
        }
    }
}

impl SearchCapabilities {
    /// Whether the given mode is usable with these capabilities.
    #[must_use]
    pub fn supports_mode(&self, mode: SearchMode) -> bool {
        match mode {
            SearchMode::Lexical => self.has_fts,
            SearchMode::Hybrid => self.has_fts && self.has_embeddings,
            SearchMode::Semantic => self.has_embeddings,
        }
    }

    /// The best default mode given current capabilities.
    #[must_use]
    pub fn best_default_mode(&self) -> SearchMode {
        if self.has_fts && self.has_embeddings {
            SearchMode::Hybrid
        } else if self.has_fts {
            SearchMode::Lexical
        } else if self.has_embeddings {
            SearchMode::Semantic
        } else {
            SearchMode::Lexical // Fallback; UI will show "no indexes" message
        }
    }

    /// Whether any search index is available at all.
    #[must_use]
    pub fn has_any_index(&self) -> bool {
        self.has_fts || self.has_embeddings
    }
}

// ---------------------------------------------------------------------------
// SearchState
// ---------------------------------------------------------------------------

/// State for the search screen.
#[derive(Debug, Default)]
pub struct SearchState {
    /// Current query text.
    pub query: String,
    /// Whether the query input has keyboard focus.
    pub query_focused: bool,
    /// Cursor position within the query string (byte offset).
    pub cursor: usize,
    /// Active search mode.
    pub mode: SearchMode,
    /// Available search capabilities (detected on screen entry).
    pub capabilities: SearchCapabilities,
    /// Current result set.
    pub results: Vec<SearchResult>,
    /// Index of the selected result in the list.
    pub selected_index: usize,
    /// Monotonic generation counter for stale-response detection.
    pub generation: u64,
    /// Whether a search request is in-flight.
    pub loading: bool,
}

impl SearchState {
    /// Enter the search screen: focus query, detect capabilities.
    pub fn enter(&mut self, capabilities: SearchCapabilities) {
        self.query_focused = true;
        self.cursor = self.query.len();
        self.capabilities = capabilities;
        // Pick the best mode for detected capabilities.
        if !self.capabilities.supports_mode(self.mode) {
            self.mode = self.capabilities.best_default_mode();
        }
    }

    /// Leave the search screen: blur focus.
    pub fn leave(&mut self) {
        self.query_focused = false;
    }

    /// Focus the query input.
    pub fn focus_query(&mut self) {
        self.query_focused = true;
        self.cursor = self.query.len();
    }

    /// Blur the query input.
    pub fn blur_query(&mut self) {
        self.query_focused = false;
    }

    /// Insert a character at the cursor position.
    ///
    /// Returns the new generation (caller should arm debounce timer).
    pub fn insert_char(&mut self, c: char) -> u64 {
        self.query.insert(self.cursor, c);
        self.cursor += c.len_utf8();
        self.generation += 1;
        self.generation
    }

    /// Delete the character before the cursor (backspace).
    ///
    /// Returns the new generation if changed, or `None` if cursor was at start.
    pub fn delete_back(&mut self) -> Option<u64> {
        if self.cursor == 0 {
            return None;
        }
        let prev = self.query[..self.cursor]
            .char_indices()
            .next_back()
            .map_or(0, |(i, _)| i);
        self.query.drain(prev..self.cursor);
        self.cursor = prev;
        self.generation += 1;
        Some(self.generation)
    }

    /// Move cursor left by one character.
    pub fn cursor_left(&mut self) {
        if self.cursor > 0 {
            self.cursor = self.query[..self.cursor]
                .char_indices()
                .next_back()
                .map_or(0, |(i, _)| i);
        }
    }

    /// Move cursor right by one character.
    pub fn cursor_right(&mut self) {
        if self.cursor < self.query.len() {
            self.cursor = self.query[self.cursor..]
                .chars()
                .next()
                .map_or(self.query.len(), |ch| self.cursor + ch.len_utf8());
        }
    }

    /// Move cursor to the start of the query.
    pub fn cursor_home(&mut self) {
        self.cursor = 0;
    }

    /// Move cursor to the end of the query.
    pub fn cursor_end(&mut self) {
        self.cursor = self.query.len();
    }

    /// Cycle to the next available search mode (skip unsupported modes).
    pub fn cycle_mode(&mut self) {
        let start = self.mode;
        let mut candidate = start.next();
        // Cycle through at most 3 modes to find a supported one.
        for _ in 0..3 {
            if self.capabilities.supports_mode(candidate) {
                self.mode = candidate;
                return;
            }
            candidate = candidate.next();
        }
        // No supported mode found (shouldn't happen if has_any_index is true).
    }

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

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

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

    /// Get the currently selected result, if any.
    #[must_use]
    pub fn selected_result(&self) -> Option<&SearchResult> {
        self.results.get(self.selected_index)
    }
}

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

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

    fn fts_only() -> SearchCapabilities {
        SearchCapabilities {
            has_fts: true,
            has_embeddings: false,
            embedding_coverage_pct: 0.0,
        }
    }

    fn full_caps() -> SearchCapabilities {
        SearchCapabilities {
            has_fts: true,
            has_embeddings: true,
            embedding_coverage_pct: 85.0,
        }
    }

    fn embeddings_only() -> SearchCapabilities {
        SearchCapabilities {
            has_fts: false,
            has_embeddings: true,
            embedding_coverage_pct: 100.0,
        }
    }

    fn no_indexes() -> SearchCapabilities {
        SearchCapabilities::default()
    }

    fn sample_result(iid: i64) -> SearchResult {
        SearchResult {
            key: EntityKey::issue(1, iid),
            title: format!("Issue #{iid}"),
            score: 0.95,
            snippet: "matched text here".into(),
            project_path: "group/project".into(),
        }
    }

    // -- SearchCapabilities tests --

    #[test]
    fn test_capabilities_supports_mode_fts_only() {
        let caps = fts_only();
        assert!(caps.supports_mode(SearchMode::Lexical));
        assert!(!caps.supports_mode(SearchMode::Hybrid));
        assert!(!caps.supports_mode(SearchMode::Semantic));
    }

    #[test]
    fn test_capabilities_supports_mode_full() {
        let caps = full_caps();
        assert!(caps.supports_mode(SearchMode::Lexical));
        assert!(caps.supports_mode(SearchMode::Hybrid));
        assert!(caps.supports_mode(SearchMode::Semantic));
    }

    #[test]
    fn test_capabilities_supports_mode_embeddings_only() {
        let caps = embeddings_only();
        assert!(!caps.supports_mode(SearchMode::Lexical));
        assert!(!caps.supports_mode(SearchMode::Hybrid));
        assert!(caps.supports_mode(SearchMode::Semantic));
    }

    #[test]
    fn test_capabilities_best_default_hybrid_when_both() {
        assert_eq!(full_caps().best_default_mode(), SearchMode::Hybrid);
    }

    #[test]
    fn test_capabilities_best_default_lexical_when_fts_only() {
        assert_eq!(fts_only().best_default_mode(), SearchMode::Lexical);
    }

    #[test]
    fn test_capabilities_best_default_semantic_when_embeddings_only() {
        assert_eq!(embeddings_only().best_default_mode(), SearchMode::Semantic);
    }

    #[test]
    fn test_capabilities_best_default_lexical_when_none() {
        assert_eq!(no_indexes().best_default_mode(), SearchMode::Lexical);
    }

    #[test]
    fn test_capabilities_has_any_index() {
        assert!(fts_only().has_any_index());
        assert!(full_caps().has_any_index());
        assert!(embeddings_only().has_any_index());
        assert!(!no_indexes().has_any_index());
    }

    // -- SearchState tests --

    #[test]
    fn test_enter_focuses_and_preserves_supported_mode() {
        let mut state = SearchState::default();
        // Default mode is Lexical, which full_caps supports — preserved.
        state.enter(full_caps());
        assert!(state.query_focused);
        assert_eq!(state.mode, SearchMode::Lexical);
    }

    #[test]
    fn test_enter_preserves_mode_if_supported() {
        let mut state = SearchState {
            mode: SearchMode::Lexical,
            ..SearchState::default()
        };
        state.enter(full_caps());
        // Lexical is supported by full_caps, so it stays.
        assert_eq!(state.mode, SearchMode::Lexical);
    }

    #[test]
    fn test_enter_overrides_unsupported_mode() {
        let mut state = SearchState {
            mode: SearchMode::Hybrid,
            ..SearchState::default()
        };
        state.enter(fts_only());
        // Hybrid requires embeddings, so fallback to Lexical.
        assert_eq!(state.mode, SearchMode::Lexical);
    }

    #[test]
    fn test_insert_char_and_cursor() {
        let mut state = SearchState::default();
        let generation1 = state.insert_char('h');
        let generation2 = state.insert_char('i');
        assert_eq!(state.query, "hi");
        assert_eq!(state.cursor, 2);
        assert_eq!(generation1, 1);
        assert_eq!(generation2, 2);
    }

    #[test]
    fn test_delete_back() {
        let mut state = SearchState::default();
        state.insert_char('a');
        state.insert_char('b');
        state.insert_char('c');

        let generation = state.delete_back();
        assert!(generation.is_some());
        assert_eq!(state.query, "ab");
        assert_eq!(state.cursor, 2);
    }

    #[test]
    fn test_delete_back_at_start_returns_none() {
        let mut state = SearchState::default();
        state.insert_char('a');
        state.cursor = 0;
        assert!(state.delete_back().is_none());
        assert_eq!(state.query, "a");
    }

    #[test]
    fn test_cursor_movement() {
        let mut state = SearchState::default();
        state.insert_char('a');
        state.insert_char('b');
        state.insert_char('c');
        assert_eq!(state.cursor, 3);

        state.cursor_left();
        assert_eq!(state.cursor, 2);
        state.cursor_left();
        assert_eq!(state.cursor, 1);
        state.cursor_right();
        assert_eq!(state.cursor, 2);
        state.cursor_home();
        assert_eq!(state.cursor, 0);
        state.cursor_end();
        assert_eq!(state.cursor, 3);
    }

    #[test]
    fn test_cursor_left_at_start_is_noop() {
        let mut state = SearchState::default();
        state.cursor_left();
        assert_eq!(state.cursor, 0);
    }

    #[test]
    fn test_cursor_right_at_end_is_noop() {
        let mut state = SearchState::default();
        state.insert_char('x');
        state.cursor_right();
        assert_eq!(state.cursor, 1);
    }

    #[test]
    fn test_cycle_mode_full_caps() {
        let mut state = SearchState {
            capabilities: full_caps(),
            mode: SearchMode::Lexical,
            ..SearchState::default()
        };

        state.cycle_mode();
        assert_eq!(state.mode, SearchMode::Hybrid);
        state.cycle_mode();
        assert_eq!(state.mode, SearchMode::Semantic);
        state.cycle_mode();
        assert_eq!(state.mode, SearchMode::Lexical);
    }

    #[test]
    fn test_cycle_mode_fts_only_stays_lexical() {
        let mut state = SearchState {
            capabilities: fts_only(),
            mode: SearchMode::Lexical,
            ..SearchState::default()
        };

        state.cycle_mode();
        // Hybrid and Semantic unsupported, wraps back to Lexical.
        assert_eq!(state.mode, SearchMode::Lexical);
    }

    #[test]
    fn test_cycle_mode_embeddings_only() {
        let mut state = SearchState {
            capabilities: embeddings_only(),
            mode: SearchMode::Semantic,
            ..SearchState::default()
        };

        state.cycle_mode();
        // Lexical and Hybrid unsupported, wraps back to Semantic.
        assert_eq!(state.mode, SearchMode::Semantic);
    }

    #[test]
    fn test_apply_results_matching_generation() {
        let mut state = SearchState::default();
        let generation = state.insert_char('q');

        let results = vec![sample_result(1), sample_result(2)];
        state.apply_results(generation, results);

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

    #[test]
    fn test_apply_results_stale_generation_discarded() {
        let mut state = SearchState::default();
        state.insert_char('q'); // gen=1
        state.insert_char('u'); // gen=2

        let stale_results = vec![sample_result(99)];
        state.apply_results(1, stale_results); // gen 1 is stale

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

    #[test]
    fn test_select_prev_next() {
        let mut state = SearchState {
            results: vec![sample_result(1), sample_result(2), sample_result(3)],
            ..SearchState::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_result() {
        let mut state = SearchState::default();
        assert!(state.selected_result().is_none());

        state.results = vec![sample_result(42)];
        let result = state.selected_result().unwrap();
        assert_eq!(result.key.iid, 42);
    }

    #[test]
    fn test_leave_blurs_focus() {
        let mut state = SearchState::default();
        state.enter(fts_only());
        assert!(state.query_focused);
        state.leave();
        assert!(!state.query_focused);
    }

    #[test]
    fn test_focus_query_moves_cursor_to_end() {
        let mut state = SearchState {
            query: "hello".into(),
            cursor: 0,
            ..SearchState::default()
        };
        state.focus_query();
        assert!(state.query_focused);
        assert_eq!(state.cursor, 5);
    }

    #[test]
    fn test_unicode_cursor_handling() {
        let mut state = SearchState::default();
        // Insert a multi-byte character.
        state.insert_char('田');
        assert_eq!(state.cursor, 3); // 田 is 3 bytes in UTF-8
        state.insert_char('中');
        assert_eq!(state.cursor, 6);

        state.cursor_left();
        assert_eq!(state.cursor, 3);
        state.cursor_right();
        assert_eq!(state.cursor, 6);

        state.delete_back();
        assert_eq!(state.query, "田");
        assert_eq!(state.cursor, 3);
    }
}
