pacsea 0.8.2

A fast, friendly TUI for browsing and installing Arch and AUR packages with built-in news and security scanning
Documentation
//! Package filtering logic for repository and AUR results.

use crate::state::{AppState, PackageItem, Source};

#[inline]
/// What: Conditionally push a `PackageItem` into the filtered results buffer.
///
/// Inputs:
/// - `cond`: When `true`, the item is appended to `out`.
/// - `it`: Candidate package record (moved into the collection when included).
/// - `out`: Destination vector accumulating filtered results.
///
/// Output:
/// - Extends `out` with `it` when `cond` evaluates to `true`; leaves `out` untouched otherwise.
///
/// Details:
/// - Keeps the filtering loop concise by encapsulating the conditional push logic.
fn return_if_true(cond: bool, it: PackageItem, out: &mut Vec<PackageItem>) {
    if cond {
        out.push(it);
    }
}

/// What: Apply current repo/AUR filters to `app.all_results`, write into `app.results`, then sort.
///
/// Inputs:
/// - `app`: Mutable application state containing `all_results`, filter toggles, and selection
///
/// Output:
/// - Updates `app.results`, applies sorting, and preserves selection when possible.
///
/// Details:
/// - Unknown official repos are included only when all official filters are enabled.
/// - Selection is restored by name when present; otherwise clamped or cleared if list is empty.
pub fn apply_filters_and_sort_preserve_selection(app: &mut AppState) {
    // Capture previous selected name to preserve when possible
    let prev_name = app.results.get(app.selected).map(|p| p.name.clone());

    // Filter from all_results into results based on toggles
    let mut filtered: Vec<PackageItem> = Vec::with_capacity(app.all_results.len());
    for it in app.all_results.iter().cloned() {
        let include = match &it.source {
            Source::Aur => app.results_filter_show_aur,
            Source::Official { repo, .. } => {
                // Unified Manjaro detection: name prefix or owner contains "manjaro" when available.
                // Prefer details_cache owner if present; fall back to name-only rule.
                let owner = app
                    .details_cache
                    .get(&it.name)
                    .map(|d| d.owner.clone())
                    .unwrap_or_default();
                if crate::index::is_manjaro_name_or_owner(&it.name, &owner) {
                    return_if_true(app.results_filter_show_manjaro, it, &mut filtered);
                    continue;
                }
                crate::logic::distro::repo_toggle_for(repo, app)
            }
        };
        if include {
            filtered.push(it);
        }
    }
    app.results = filtered;
    // Invalidate sort caches since results changed
    crate::logic::invalidate_sort_caches(app);
    // Apply existing sort policy and preserve selection
    crate::logic::sort_results_preserve_selection(app);
    // Restore by name if possible
    if let Some(name) = prev_name {
        if let Some(pos) = app.results.iter().position(|p| p.name == name) {
            app.selected = pos;
            app.list_state.select(Some(pos));
        } else if !app.results.is_empty() {
            app.selected = app.selected.min(app.results.len() - 1);
            app.list_state.select(Some(app.selected));
        } else {
            app.selected = 0;
            app.list_state.select(None);
        }
    } else if app.results.is_empty() {
        app.selected = 0;
        app.list_state.select(None);
    } else {
        app.selected = app.selected.min(app.results.len() - 1);
        app.list_state.select(Some(app.selected));
    }
}

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

    fn item_official(name: &str, repo: &str) -> PackageItem {
        PackageItem {
            name: name.to_string(),
            version: "1.0".to_string(),
            description: format!("{name} desc"),
            source: Source::Official {
                repo: repo.to_string(),
                arch: "x86_64".to_string(),
            },
            popularity: None,
            out_of_date: None,
            orphaned: false,
        }
    }

    #[test]
    /// What: Ensure repo/AUR filters include only enabled repositories while keeping selection stable.
    ///
    /// Inputs:
    /// - `app`: `AppState` seeded with mixed official/AUR results and selective filter toggles.
    ///
    /// Output:
    /// - Results contain solely core packages after filtering; selection index remains valid.
    ///
    /// Details:
    /// - Disables AUR/extra/multilib toggles to confirm `apply_filters_and_sort_preserve_selection`
    ///   respects flags and prunes disabled repositories.
    fn apply_filters_and_preserve_selection() {
        let mut app = AppState {
            all_results: vec![
                PackageItem {
                    name: "aur1".into(),
                    version: "1".into(),
                    description: String::new(),
                    source: Source::Aur,
                    popularity: Some(1.0),
                    out_of_date: None,
                    orphaned: false,
                },
                item_official("core1", "core"),
                item_official("extra1", "extra"),
                item_official("other1", "community"),
            ],
            results_filter_show_aur: false,
            results_filter_show_core: true,
            results_filter_show_extra: false,
            results_filter_show_multilib: false,
            ..Default::default()
        };
        apply_filters_and_sort_preserve_selection(&mut app);
        assert!(app.results.iter().all(
            |p| matches!(&p.source, Source::Official{repo, ..} if repo.eq_ignore_ascii_case("core"))
        ));
    }

    #[test]
    /// What: Verify `CachyOS` and `EOS` toggles act independently when filtering official repos.
    ///
    /// Inputs:
    /// - `app`: `AppState` containing `CachyOS` and `EndeavourOS` entries with toggle combinations.
    ///
    /// Output:
    /// - `CachyOS` packages persist while `EOS` entries are removed per toggle state.
    ///
    /// Details:
    /// - Confirms `CachyOS` inclusion does not implicitly re-enable `EOS` repositories.
    fn apply_filters_cachyos_and_eos_interaction() {
        let mut app = AppState {
            all_results: vec![
                PackageItem {
                    name: "cx".into(),
                    version: "1".into(),
                    description: String::new(),
                    source: Source::Official {
                        repo: "cachyos-core".into(),
                        arch: "x86_64".into(),
                    },
                    popularity: None,
                    out_of_date: None,
                    orphaned: false,
                },
                PackageItem {
                    name: "ey".into(),
                    version: "1".into(),
                    description: String::new(),
                    source: Source::Official {
                        repo: "endeavouros".into(),
                        arch: "x86_64".into(),
                    },
                    popularity: None,
                    out_of_date: None,
                    orphaned: false,
                },
                item_official("core1", "core"),
            ],
            results_filter_show_core: true,
            results_filter_show_extra: true,
            results_filter_show_multilib: true,
            results_filter_show_eos: false,
            results_filter_show_cachyos: true,
            ..Default::default()
        };
        apply_filters_and_sort_preserve_selection(&mut app);
        assert!(app.results.iter().any(|p| match &p.source {
            Source::Official { repo, .. } => repo.to_lowercase().starts_with("cachyos"),
            Source::Aur => false,
        }));
        assert!(app.results.iter().all(|p| match &p.source {
            Source::Official { repo, .. } => !repo.eq_ignore_ascii_case("endeavouros"),
            Source::Aur => true,
        }));
    }

    #[test]
    /// What: Validate inclusion rules for unknown official repositories relative to toggle coverage.
    ///
    /// Inputs:
    /// - `app`: `AppState` with an unfamiliar official repo plus standard core entry.
    ///
    /// Output:
    /// - Unknown repo excluded when any official toggle is off, then included once all are enabled.
    ///
    /// Details:
    /// - Demonstrates that enabling the remaining official toggle (`multilib`) widens acceptance to
    ///   previously filtered repos.
    fn logic_filter_unknown_official_inclusion_policy() {
        let mut app = AppState {
            all_results: vec![
                PackageItem {
                    name: "x1".into(),
                    version: "1".into(),
                    description: String::new(),
                    source: Source::Official {
                        repo: "weirdrepo".into(),
                        arch: "x86_64".into(),
                    },
                    popularity: None,
                    out_of_date: None,
                    orphaned: false,
                },
                item_official("core1", "core"),
            ],
            results_filter_show_aur: true,
            results_filter_show_core: true,
            results_filter_show_extra: true,
            results_filter_show_multilib: false,
            results_filter_show_eos: true,
            results_filter_show_cachyos: true,
            ..Default::default()
        };
        apply_filters_and_sort_preserve_selection(&mut app);
        assert!(app.results.iter().all(|p| match &p.source {
            Source::Official { repo, .. } => repo.eq_ignore_ascii_case("core"),
            Source::Aur => false,
        }));

        app.results_filter_show_multilib = true;
        apply_filters_and_sort_preserve_selection(&mut app);
        assert!(app.results.iter().any(|p| match &p.source {
            Source::Official { repo, .. } => repo.eq_ignore_ascii_case("weirdrepo"),
            Source::Aur => false,
        }));
    }
}