use std::{cmp::Reverse, rc::Rc, sync::Arc};

use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol::schema as acp;
use agent_servers::AgentServer;

use anyhow::Result;
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{
    Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
    TaskExt, WeakEntity,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::SettingsStore;
use ui::{DocumentationAside, IntoElement, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;

use crate::ui::{
    ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem, documentation_aside_side,
};

pub type ModelSelector = Picker<ModelPickerDelegate>;

pub fn acp_model_selector(
    selector: Rc<dyn AgentModelSelector>,
    agent_server: Rc<dyn AgentServer>,
    fs: Arc<dyn Fs>,
    focus_handle: FocusHandle,
    window: &mut Window,
    cx: &mut Context<ModelSelector>,
) -> ModelSelector {
    let delegate = ModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
    Picker::list(delegate, window, cx)
        .show_scrollbar(true)
        .width(rems(20.))
        .max_height(Some(rems(20.).into()))
}

enum ModelPickerEntry {
    Separator(SharedString),
    Model(AgentModelInfo, bool),
}

pub struct ModelPickerDelegate {
    selector: Rc<dyn AgentModelSelector>,
    agent_server: Rc<dyn AgentServer>,
    fs: Arc<dyn Fs>,
    filtered_entries: Vec<ModelPickerEntry>,
    models: Option<AgentModelList>,
    selected_index: usize,
    selected_description: Option<(usize, SharedString)>,
    selected_model: Option<AgentModelInfo>,
    favorites: HashSet<acp::ModelId>,
    _refresh_models_task: Task<()>,
    _settings_subscription: Subscription,
    focus_handle: FocusHandle,
}

impl ModelPickerDelegate {
    fn new(
        selector: Rc<dyn AgentModelSelector>,
        agent_server: Rc<dyn AgentServer>,
        fs: Arc<dyn Fs>,
        focus_handle: FocusHandle,
        window: &mut Window,
        cx: &mut Context<ModelSelector>,
    ) -> Self {
        let rx = selector.watch(cx);
        let refresh_models_task = {
            cx.spawn_in(window, {
                async move |this, cx| {
                    async fn refresh(
                        this: &WeakEntity<Picker<ModelPickerDelegate>>,
                        cx: &mut AsyncWindowContext,
                    ) -> Result<()> {
                        let (models_task, selected_model_task) = this.update(cx, |this, cx| {
                            (
                                this.delegate.selector.list_models(cx),
                                this.delegate.selector.selected_model(cx),
                            )
                        })?;

                        let (models, selected_model) =
                            futures::join!(models_task, selected_model_task);

                        this.update_in(cx, |this, window, cx| {
                            this.delegate.models = models.ok();
                            this.delegate.selected_model = selected_model.ok();
                            this.refresh(window, cx)
                        })
                    }

                    refresh(&this, cx).await.log_err();
                    if let Some(mut rx) = rx {
                        while let Ok(()) = rx.recv().await {
                            refresh(&this, cx).await.log_err();
                        }
                    }
                }
            })
        };

        let agent_server_for_subscription = agent_server.clone();
        let settings_subscription =
            cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
                // Only refresh if the favorites actually changed to avoid redundant work
                // when other settings are modified (e.g., user editing settings.json)
                let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
                if new_favorites != picker.delegate.favorites {
                    picker.delegate.favorites = new_favorites;
                    picker.refresh(window, cx);
                }
            });
        let favorites = agent_server.favorite_model_ids(cx);

        Self {
            selector,
            agent_server,
            fs,
            filtered_entries: Vec::new(),
            models: None,
            selected_model: None,
            selected_index: 0,
            selected_description: None,
            favorites,
            _refresh_models_task: refresh_models_task,
            _settings_subscription: settings_subscription,
            focus_handle,
        }
    }

    pub fn active_model(&self) -> Option<&AgentModelInfo> {
        self.selected_model.as_ref()
    }

    pub fn favorites_count(&self) -> usize {
        self.favorites.len()
    }

    pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
        if self.favorites.is_empty() {
            return;
        }

        let Some(models) = &self.models else {
            return;
        };

        let all_models: Vec<&AgentModelInfo> = match models {
            AgentModelList::Flat(list) => list.iter().collect(),
            AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
        };

        let favorite_models: Vec<_> = all_models
            .into_iter()
            .filter(|model| self.favorites.contains(&model.id))
            .unique_by(|model| &model.id)
            .collect();

        if favorite_models.is_empty() {
            return;
        }

        let current_id = self.selected_model.as_ref().map(|m| &m.id);

        let current_index_in_favorites = current_id
            .and_then(|id| favorite_models.iter().position(|m| &m.id == id))
            .unwrap_or(usize::MAX);

        let next_index = if current_index_in_favorites == usize::MAX {
            0
        } else {
            (current_index_in_favorites + 1) % favorite_models.len()
        };

        let next_model = favorite_models[next_index].clone();

        self.agent_server
            .set_default_model(Some(next_model.id.clone()), self.fs.clone(), cx);

        self.selector
            .select_model(next_model.id.clone(), cx)
            .detach_and_log_err(cx);

        self.selected_model = Some(next_model);

        // Keep the picker selection aligned with the newly-selected model
        if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
            matches!(entry, ModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
        }) {
            self.set_selected_index(new_index, window, cx);
        } else {
            cx.notify();
        }
    }
}

impl PickerDelegate for ModelPickerDelegate {
    type ListItem = AnyElement;

    fn match_count(&self) -> usize {
        self.filtered_entries.len()
    }

    fn selected_index(&self) -> usize {
        self.selected_index
    }

    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
        cx.notify();
    }

    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
        match self.filtered_entries.get(ix) {
            Some(ModelPickerEntry::Model(_, _)) => true,
            Some(ModelPickerEntry::Separator(_)) | None => false,
        }
    }

    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
        "Select a model…".into()
    }

    fn update_matches(
        &mut self,
        query: String,
        window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> Task<()> {
        let favorites = self.favorites.clone();

        cx.spawn_in(window, async move |this, cx| {
            let filtered_models = match this
                .read_with(cx, |this, cx| {
                    this.delegate.models.clone().map(move |models| {
                        fuzzy_search(models, query, cx.background_executor().clone())
                    })
                })
                .ok()
                .flatten()
            {
                Some(task) => task.await,
                None => AgentModelList::Flat(vec![]),
            };

            this.update_in(cx, |this, window, cx| {
                this.delegate.filtered_entries =
                    info_list_to_picker_entries(filtered_models, &favorites);
                // Finds the currently selected model in the list
                let new_index = this
                    .delegate
                    .selected_model
                    .as_ref()
                    .and_then(|selected| {
                        this.delegate.filtered_entries.iter().position(|entry| {
                            if let ModelPickerEntry::Model(model_info, _) = entry {
                                model_info.id == selected.id
                            } else {
                                false
                            }
                        })
                    })
                    .unwrap_or(0);
                this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
                cx.notify();
            })
            .ok();
        })
    }

    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
        if let Some(ModelPickerEntry::Model(model_info, _)) =
            self.filtered_entries.get(self.selected_index)
        {
            self.agent_server
                .set_default_model(Some(model_info.id.clone()), self.fs.clone(), cx);

            self.selector
                .select_model(model_info.id.clone(), cx)
                .detach_and_log_err(cx);
            self.selected_model = Some(model_info.clone());
            let current_index = self.selected_index;
            self.set_selected_index(current_index, window, cx);

            cx.emit(DismissEvent);
        }
    }

    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
        cx.defer_in(window, |picker, window, cx| {
            picker.set_query("", window, cx);
        });
    }

    fn render_match(
        &self,
        ix: usize,
        selected: bool,
        _: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> Option<Self::ListItem> {
        match self.filtered_entries.get(ix)? {
            ModelPickerEntry::Separator(title) => {
                Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
            }
            ModelPickerEntry::Model(model_info, is_favorite) => {
                let is_selected = Some(model_info) == self.selected_model.as_ref();

                let is_favorite = *is_favorite;
                let handle_action_click = {
                    let model_id = model_info.id.clone();
                    let fs = self.fs.clone();
                    let agent_server = self.agent_server.clone();

                    cx.listener(move |_, _, _, cx| {
                        agent_server.toggle_favorite_model(
                            model_id.clone(),
                            !is_favorite,
                            fs.clone(),
                            cx,
                        );
                    })
                };

                let model_cost = model_info.cost.clone();

                Some(
                    div()
                        .id(("model-picker-menu-child", ix))
                        .when_some(model_info.description.clone(), |this, description| {
                            this.on_hover(cx.listener(move |menu, hovered, _, cx| {
                                if *hovered {
                                    menu.delegate.selected_description =
                                        Some((ix, description.clone()));
                                } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
                                    menu.delegate.selected_description = None;
                                }
                                cx.notify();
                            }))
                        })
                        .child(
                            ModelSelectorListItem::new(ix, model_info.name.clone())
                                .map(|this| match &model_info.icon {
                                    Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
                                    Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
                                    None => this,
                                })
                                .is_selected(is_selected)
                                .is_focused(selected)
                                .is_latest(model_info.is_latest)
                                .is_favorite(is_favorite)
                                .on_toggle_favorite(handle_action_click)
                                .cost_info(model_cost)
                        )
                        .into_any_element(),
                )
            }
        }
    }

    fn documentation_aside(
        &self,
        _window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> Option<ui::DocumentationAside> {
        self.selected_description.as_ref().map(|(_, description)| {
            let description = description.clone();

            let side = documentation_aside_side(cx);

            DocumentationAside::new(
                side,
                Rc::new(move |_| Label::new(description.clone()).into_any_element()),
            )
        })
    }

    fn documentation_aside_index(&self) -> Option<usize> {
        self.selected_description.as_ref().map(|(ix, _)| *ix)
    }

    fn render_footer(
        &self,
        _window: &mut Window,
        _cx: &mut Context<Picker<Self>>,
    ) -> Option<AnyElement> {
        let focus_handle = self.focus_handle.clone();

        if !self.selector.should_render_footer() {
            return None;
        }

        Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
    }
}

fn info_list_to_picker_entries(
    model_list: AgentModelList,
    favorites: &HashSet<acp::ModelId>,
) -> Vec<ModelPickerEntry> {
    let mut entries = Vec::new();

    let all_models: Vec<_> = match &model_list {
        AgentModelList::Flat(list) => list.iter().collect(),
        AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
    };

    let favorite_models: Vec<_> = all_models
        .iter()
        .filter(|m| favorites.contains(&m.id))
        .unique_by(|m| &m.id)
        .collect();

    let has_favorites = !favorite_models.is_empty();
    if has_favorites {
        entries.push(ModelPickerEntry::Separator("Favorite".into()));
        for model in favorite_models {
            entries.push(ModelPickerEntry::Model((*model).clone(), true));
        }
    }

    match model_list {
        AgentModelList::Flat(list) => {
            if has_favorites {
                entries.push(ModelPickerEntry::Separator("All".into()));
            }
            for model in list {
                let is_favorite = favorites.contains(&model.id);
                entries.push(ModelPickerEntry::Model(model, is_favorite));
            }
        }
        AgentModelList::Grouped(index_map) => {
            for (group_name, models) in index_map {
                entries.push(ModelPickerEntry::Separator(group_name.0));
                for model in models {
                    let is_favorite = favorites.contains(&model.id);
                    entries.push(ModelPickerEntry::Model(model, is_favorite));
                }
            }
        }
    }

    entries
}

async fn fuzzy_search(
    model_list: AgentModelList,
    query: String,
    executor: BackgroundExecutor,
) -> AgentModelList {
    async fn fuzzy_search_list(
        model_list: Vec<AgentModelInfo>,
        query: &str,
        executor: BackgroundExecutor,
    ) -> Vec<AgentModelInfo> {
        let candidates = model_list
            .iter()
            .enumerate()
            .map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref()))
            .collect::<Vec<_>>();
        let mut matches = match_strings(
            &candidates,
            query,
            false,
            true,
            100,
            &Default::default(),
            executor,
        )
        .await;

        matches.sort_unstable_by_key(|mat| {
            let candidate = &candidates[mat.candidate_id];
            (Reverse(OrderedFloat(mat.score)), candidate.id)
        });

        matches
            .into_iter()
            .map(|mat| model_list[mat.candidate_id].clone())
            .collect()
    }

    match model_list {
        AgentModelList::Flat(model_list) => {
            AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await)
        }
        AgentModelList::Grouped(index_map) => {
            let groups =
                futures::future::join_all(index_map.into_iter().map(|(group_name, models)| {
                    fuzzy_search_list(models, &query, executor.clone())
                        .map(|results| (group_name, results))
                }))
                .await;
            AgentModelList::Grouped(IndexMap::from_iter(
                groups
                    .into_iter()
                    .filter(|(_, results)| !results.is_empty()),
            ))
        }
    }
}

#[cfg(test)]
mod tests {
    use acp_thread::AgentConnection;
    use fs::FakeFs;
    use gpui::{App, Entity, TestAppContext, VisualTestContext};
    use parking_lot::Mutex;
    use project::{AgentId, Project};
    use std::{any::Any, cell::RefCell};

    use super::*;

    fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList {
        AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map(
            |(group, models)| {
                (
                    acp_thread::AgentModelGroupName(group.to_string().into()),
                    models
                        .into_iter()
                        .map(|model| acp_thread::AgentModelInfo {
                            id: acp::ModelId::new(model.to_string()),
                            name: model.to_string().into(),
                            description: None,
                            icon: None,
                            is_latest: false,
                            cost: None,
                        })
                        .collect::<Vec<_>>(),
                )
            },
        )))
    }

    fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) {
        let AgentModelList::Grouped(groups) = result else {
            panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result);
        };

        assert_eq!(
            groups.len(),
            expected.len(),
            "Number of groups doesn't match"
        );

        for (i, (expected_group, expected_models)) in expected.iter().enumerate() {
            let (actual_group, actual_models) = groups.get_index(i).unwrap();
            assert_eq!(
                actual_group.0.as_ref(),
                *expected_group,
                "Group at position {} doesn't match expected group",
                i
            );
            assert_eq!(
                actual_models.len(),
                expected_models.len(),
                "Number of models in group {} doesn't match",
                expected_group
            );

            for (j, expected_model_name) in expected_models.iter().enumerate() {
                assert_eq!(
                    actual_models[j].name, *expected_model_name,
                    "Model at position {} in group {} doesn't match expected model",
                    j, expected_group
                );
            }
        }
    }

    fn create_favorites(models: Vec<&str>) -> HashSet<acp::ModelId> {
        models
            .into_iter()
            .map(|m| acp::ModelId::new(m.to_string()))
            .collect()
    }

    fn get_entry_model_ids(entries: &[ModelPickerEntry]) -> Vec<&str> {
        entries
            .iter()
            .filter_map(|entry| match entry {
                ModelPickerEntry::Model(info, _) => Some(info.id.0.as_ref()),
                _ => None,
            })
            .collect()
    }

    #[gpui::test]
    fn confirming_model_saves_selected_model_as_default(cx: &mut TestAppContext) {
        cx.update(|cx| {
            let settings_store = settings::SettingsStore::test(cx);
            cx.set_global(settings_store);
            theme_settings::init(theme::LoadThemes::JustBase, cx);
            editor::init(cx);
        });

        let agent_server = Rc::new(TestAgentServer::default());
        let model_selector = Rc::new(TestModelSelector::new());
        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());

        let window_handle = cx.add_window({
            let agent_server = agent_server.clone();
            let model_selector = model_selector.clone();
            move |window, cx| {
                let selector: Rc<dyn AgentModelSelector> = model_selector.clone();
                let agent_server: Rc<dyn AgentServer> = agent_server.clone();
                acp_model_selector(selector, agent_server, fs, cx.focus_handle(), window, cx)
            }
        });
        cx.run_until_parked();

        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
        window_handle
            .update(&mut cx, |picker, window, cx| {
                picker.delegate.set_selected_index(1, window, cx);
                picker.delegate.confirm(false, window, cx);
            })
            .unwrap();

        assert_eq!(
            agent_server.saved_defaults.lock().as_slice(),
            &[Some(acp::ModelId::new("manual"))]
        );
        assert_eq!(
            model_selector.selected_models.borrow().as_slice(),
            &[acp::ModelId::new("manual")]
        );
    }

    #[derive(Default)]
    struct TestAgentServer {
        saved_defaults: Arc<Mutex<Vec<Option<acp::ModelId>>>>,
    }

    impl AgentServer for TestAgentServer {
        fn logo(&self) -> IconName {
            IconName::ZedAssistant
        }

        fn agent_id(&self) -> AgentId {
            AgentId::new("test-agent")
        }

        fn connect(
            &self,
            _delegate: agent_servers::AgentServerDelegate,
            _project: Entity<Project>,
            _cx: &mut App,
        ) -> Task<anyhow::Result<Rc<dyn AgentConnection>>> {
            Task::ready(Err(anyhow::anyhow!("test agent server cannot connect")))
        }

        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
            self
        }

        fn set_default_model(
            &self,
            model_id: Option<acp::ModelId>,
            _fs: Arc<dyn Fs>,
            _cx: &mut App,
        ) {
            self.saved_defaults.lock().push(model_id);
        }
    }

    struct TestModelSelector {
        models: Vec<AgentModelInfo>,
        selected_model: RefCell<AgentModelInfo>,
        selected_models: RefCell<Vec<acp::ModelId>>,
    }

    impl TestModelSelector {
        fn new() -> Self {
            let models = vec![
                AgentModelInfo {
                    id: acp::ModelId::new("auto"),
                    name: "Auto".into(),
                    description: None,
                    icon: None,
                    is_latest: false,
                    cost: None,
                },
                AgentModelInfo {
                    id: acp::ModelId::new("manual"),
                    name: "Manual".into(),
                    description: None,
                    icon: None,
                    is_latest: false,
                    cost: None,
                },
            ];

            Self {
                selected_model: RefCell::new(models[0].clone()),
                models,
                selected_models: RefCell::new(Vec::new()),
            }
        }
    }

    impl AgentModelSelector for TestModelSelector {
        fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
            Task::ready(Ok(AgentModelList::Flat(self.models.clone())))
        }

        fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
            self.selected_models.borrow_mut().push(model_id.clone());
            if let Some(model) = self.models.iter().find(|model| model.id == model_id) {
                *self.selected_model.borrow_mut() = model.clone();
            }
            Task::ready(Ok(()))
        }

        fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
            Task::ready(Ok(self.selected_model.borrow().clone()))
        }
    }

    fn get_entry_labels(entries: &[ModelPickerEntry]) -> Vec<&str> {
        entries
            .iter()
            .map(|entry| match entry {
                ModelPickerEntry::Model(info, _) => info.id.0.as_ref(),
                ModelPickerEntry::Separator(s) => &s,
            })
            .collect()
    }

    #[gpui::test]
    async fn test_fuzzy_match(cx: &mut TestAppContext) {
        let models = create_model_list(vec![
            (
                "zed",
                vec![
                    "Claude 3.7 Sonnet",
                    "Claude 3.7 Sonnet Thinking",
                    "gpt-5",
                    "gpt-5-mini",
                ],
            ),
            ("openai", vec!["gpt-3.5-turbo", "gpt-5", "gpt-5-mini"]),
            ("ollama", vec!["mistral", "deepseek"]),
        ]);

        // Results should preserve models order whenever possible.
        // In the case below, `zed/gpt-5-mini` and `openai/gpt-5-mini` have identical
        // similarity scores, but `zed/gpt-5-mini` was higher in the models list,
        // so it should appear first in the results.
        let results = fuzzy_search(models.clone(), "mini".into(), cx.executor()).await;
        assert_models_eq(
            results,
            vec![("zed", vec!["gpt-5-mini"]), ("openai", vec!["gpt-5-mini"])],
        );

        // Fuzzy search - test with specific model name
        let results = fuzzy_search(models.clone(), "mistral".into(), cx.executor()).await;
        assert_models_eq(results, vec![("ollama", vec!["mistral"])]);
    }

    #[gpui::test]
    fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
        let models = create_model_list(vec![
            ("zed", vec!["zed/claude", "zed/gemini"]),
            ("openai", vec!["openai/gpt-5"]),
        ]);
        let favorites = create_favorites(vec!["zed/gemini"]);

        let entries = info_list_to_picker_entries(models, &favorites);

        assert!(matches!(
            entries.first(),
            Some(ModelPickerEntry::Separator(s)) if s == "Favorite"
        ));

        let model_ids = get_entry_model_ids(&entries);
        assert_eq!(model_ids[0], "zed/gemini");
    }

    #[gpui::test]
    fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
        let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
        let favorites = create_favorites(vec![]);

        let entries = info_list_to_picker_entries(models, &favorites);

        assert!(matches!(
            entries.first(),
            Some(ModelPickerEntry::Separator(s)) if s == "zed"
        ));
    }

    #[gpui::test]
    fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
        let models = create_model_list(vec![
            ("zed", vec!["zed/claude", "zed/gemini"]),
            ("openai", vec!["openai/gpt-5"]),
        ]);
        let favorites = create_favorites(vec!["zed/claude"]);

        let entries = info_list_to_picker_entries(models, &favorites);

        for entry in &entries {
            if let ModelPickerEntry::Model(info, is_favorite) = entry {
                if info.id.0.as_ref() == "zed/claude" {
                    assert!(is_favorite, "zed/claude should be a favorite");
                } else {
                    assert!(!is_favorite, "{} should not be a favorite", info.id.0);
                }
            }
        }
    }

    #[gpui::test]
    fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
        let models = create_model_list(vec![
            ("zed", vec!["zed/claude", "zed/gemini"]),
            ("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
        ]);
        let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);

        let entries = info_list_to_picker_entries(models, &favorites);
        let model_ids = get_entry_model_ids(&entries);

        assert_eq!(model_ids[0], "zed/gemini");
        assert_eq!(model_ids[1], "openai/gpt-5");

        assert!(model_ids[2..].contains(&"zed/gemini"));
        assert!(model_ids[2..].contains(&"openai/gpt-5"));
    }

    #[gpui::test]
    fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
        let models = create_model_list(vec![
            ("Recommended", vec!["zed/claude", "anthropic/claude"]),
            ("Zed", vec!["zed/claude", "zed/gpt-5"]),
            ("Antropic", vec!["anthropic/claude"]),
            ("OpenAI", vec!["openai/gpt-5"]),
        ]);

        let favorites = create_favorites(vec!["zed/claude"]);

        let entries = info_list_to_picker_entries(models, &favorites);
        let labels = get_entry_labels(&entries);

        assert_eq!(
            labels,
            vec![
                "Favorite",
                "zed/claude",
                "Recommended",
                "zed/claude",
                "anthropic/claude",
                "Zed",
                "zed/claude",
                "zed/gpt-5",
                "Antropic",
                "anthropic/claude",
                "OpenAI",
                "openai/gpt-5"
            ]
        );
    }

    #[gpui::test]
    fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
        let models = AgentModelList::Flat(vec![
            acp_thread::AgentModelInfo {
                id: acp::ModelId::new("zed/claude".to_string()),
                name: "Claude".into(),
                description: None,
                icon: None,
                is_latest: false,
                cost: None,
            },
            acp_thread::AgentModelInfo {
                id: acp::ModelId::new("zed/gemini".to_string()),
                name: "Gemini".into(),
                description: None,
                icon: None,
                is_latest: false,
                cost: None,
            },
        ]);
        let favorites = create_favorites(vec!["zed/gemini"]);

        let entries = info_list_to_picker_entries(models, &favorites);

        assert!(matches!(
            entries.first(),
            Some(ModelPickerEntry::Separator(s)) if s == "Favorite"
        ));

        assert!(entries.iter().any(|e| matches!(
            e,
            ModelPickerEntry::Separator(s) if s == "All"
        )));
    }

    #[gpui::test]
    fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
        let empty_favorites: HashSet<acp::ModelId> = HashSet::default();
        assert_eq!(empty_favorites.len(), 0);

        let one_favorite = create_favorites(vec!["model-a"]);
        assert_eq!(one_favorite.len(), 1);

        let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
        assert_eq!(multiple_favorites.len(), 3);

        let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
        assert_eq!(with_duplicates.len(), 2);
    }

    #[gpui::test]
    fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
        let models = AgentModelList::Flat(vec![
            acp_thread::AgentModelInfo {
                id: acp::ModelId::new("favorite-model".to_string()),
                name: "Favorite".into(),
                description: None,
                icon: None,
                is_latest: false,
                cost: None,
            },
            acp_thread::AgentModelInfo {
                id: acp::ModelId::new("regular-model".to_string()),
                name: "Regular".into(),
                description: None,
                icon: None,
                is_latest: false,
                cost: None,
            },
        ]);
        let favorites = create_favorites(vec!["favorite-model"]);

        let entries = info_list_to_picker_entries(models, &favorites);

        for entry in &entries {
            if let ModelPickerEntry::Model(info, is_favorite) = entry {
                if info.id.0.as_ref() == "favorite-model" {
                    assert!(*is_favorite, "favorite-model should have is_favorite=true");
                } else if info.id.0.as_ref() == "regular-model" {
                    assert!(!*is_favorite, "regular-model should have is_favorite=false");
                }
            }
        }
    }
}
