use serde::Serialize;
use std::collections::BTreeSet;

pub const PAGE_FOCUS_KEYBOARD_REDUCER_CONTRACT_NAME: &str = "rust_page_focus_keyboard_reducer_v1";

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageFocusKeyboardReducerContract {
    pub contract_name: &'static str,
    pub actions: BTreeSet<&'static str>,
}

impl Default for PageFocusKeyboardReducerContract {
    fn default() -> Self {
        Self {
            contract_name: PAGE_FOCUS_KEYBOARD_REDUCER_CONTRACT_NAME,
            actions: BTreeSet::from([
                "normalize",
                "focus",
                "move_next",
                "move_previous",
                "move_home",
                "move_end",
                "expand",
                "collapse",
                "toggle",
                "open",
                "context_menu",
            ]),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TreeShellFocusState {
    pub focused_id: Option<String>,
}

impl TreeShellFocusState {
    pub fn normalize(&self, visible_ids: &[String]) -> Self {
        if let Some(focused_id) = self.focused_id.as_deref() {
            if visible_ids.iter().any(|id| id == focused_id) {
                return self.clone();
            }
        }
        Self {
            focused_id: visible_ids.first().cloned(),
        }
    }

    pub fn move_next(&self, visible_ids: &[String]) -> Self {
        self.move_by(visible_ids, 1)
    }

    pub fn move_previous(&self, visible_ids: &[String]) -> Self {
        self.move_by(visible_ids, -1)
    }

    pub fn move_home(&self, visible_ids: &[String]) -> Self {
        Self {
            focused_id: visible_ids.first().cloned(),
        }
    }

    pub fn move_end(&self, visible_ids: &[String]) -> Self {
        Self {
            focused_id: visible_ids.last().cloned(),
        }
    }

    fn move_by(&self, visible_ids: &[String], offset: isize) -> Self {
        if visible_ids.is_empty() {
            return Self::default();
        }
        let current_index = self
            .focused_id
            .as_deref()
            .and_then(|focused_id| visible_ids.iter().position(|id| id == focused_id))
            .unwrap_or(0);
        let next_index =
            (current_index as isize + offset).clamp(0, (visible_ids.len() - 1) as isize) as usize;
        Self {
            focused_id: Some(visible_ids[next_index].clone()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        PageFocusKeyboardReducerContract, TreeShellFocusState,
        PAGE_FOCUS_KEYBOARD_REDUCER_CONTRACT_NAME,
    };

    fn ids(values: &[&str]) -> Vec<String> {
        values.iter().map(|value| (*value).to_string()).collect()
    }

    #[test]
    fn tree_shell_focus_state_normalizes_and_moves_within_visible_rows() {
        let visible_ids = ids(&["doc:a", "doc:b", "doc:c"]);
        let state = TreeShellFocusState {
            focused_id: Some("missing".into()),
        }
        .normalize(&visible_ids);
        assert_eq!(state.focused_id.as_deref(), Some("doc:a"));

        let state = state
            .move_next(&visible_ids)
            .move_next(&visible_ids)
            .move_next(&visible_ids);
        assert_eq!(state.focused_id.as_deref(), Some("doc:c"));
        assert_eq!(
            state.move_previous(&visible_ids).focused_id.as_deref(),
            Some("doc:b")
        );
        assert_eq!(
            state.move_home(&visible_ids).focused_id.as_deref(),
            Some("doc:a")
        );
        assert_eq!(
            state.move_end(&visible_ids).focused_id.as_deref(),
            Some("doc:c")
        );
    }

    #[test]
    fn tree_shell_focus_state_empty_rows_clear_focus() {
        let state = TreeShellFocusState {
            focused_id: Some("doc:a".into()),
        }
        .normalize(&[]);
        assert_eq!(state.focused_id, None);
        assert_eq!(state.move_next(&[]).focused_id, None);
    }

    #[test]
    fn tree_shell_page_focus_keyboard_reducer_contract_exposes_supported_actions() {
        let contract = PageFocusKeyboardReducerContract::default();
        assert_eq!(
            contract.contract_name,
            PAGE_FOCUS_KEYBOARD_REDUCER_CONTRACT_NAME
        );
        assert!(contract.actions.contains("focus"));
        assert!(contract.actions.contains("move_next"));
        assert!(contract.actions.contains("move_previous"));
        assert!(contract.actions.contains("move_home"));
        assert!(contract.actions.contains("move_end"));
        assert!(contract.actions.contains("expand"));
        assert!(contract.actions.contains("collapse"));
        assert!(contract.actions.contains("toggle"));
        assert!(contract.actions.contains("open"));
        assert!(contract.actions.contains("context_menu"));
    }
}
