// SPDX-License-Identifier: GPL-3.0-or-later
//! Tests for newer filter features (relative dates, etc.).
use cfait::context::TestContext;
use cfait::model::{Task, TaskStatus};
use cfait::store::{FilterOptions, TaskStore};
use chrono::{Datelike, Duration, Local};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

#[test]
fn test_relative_start_date_parsing() {
    let aliases = std::collections::HashMap::new();
    let now = Local::now().date_naive();

    // Test relative start date with days
    let t1 = Task::new("Task ^2d", &aliases, None);
    assert!(t1.dtstart.is_some());
    assert_eq!(t1.dtstart.unwrap().to_date_naive(), now + Duration::days(2));

    // Test relative start date with weeks
    let t2 = Task::new("Task ^1w", &aliases, None);
    assert!(t2.dtstart.is_some());
    assert_eq!(t2.dtstart.unwrap().to_date_naive(), now + Duration::days(7));

    // Test start date with "tomorrow" keyword
    let t3 = Task::new("Task ^tomorrow", &aliases, None);
    assert!(t3.dtstart.is_some());
    assert_eq!(t3.dtstart.unwrap().to_date_naive(), now + Duration::days(1));
}

#[test]
fn test_start_date_filter_with_relative_dates() {
    let aliases = std::collections::HashMap::new();
    let now = Local::now().date_naive();

    // Create task with start date 3 days from now
    let future_task = Task::new(
        &format!(
            "Future Task ^{}",
            (now + Duration::days(3)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );

    // Create task with start date today
    let today_task = Task::new("Today Task ^today", &aliases, None);

    // Filter for tasks starting after 2 days from now
    // future_task (3 days) > 2 days, so it should match
    assert!(future_task.matches_search_term("^>2d"));
    // today_task (0 days) is NOT > 2 days, so it should not match
    assert!(!today_task.matches_search_term("^>2d"));

    // Filter for tasks starting before 5 days from now
    // Both tasks are before 5 days, so both should match
    assert!(future_task.matches_search_term("^<5d"));
    assert!(today_task.matches_search_term("^<5d"));
}

#[test]
fn test_not_set_operator_with_exclamation() {
    let aliases = std::collections::HashMap::new();

    // Task with no start date
    let no_start = Task::new("Task without start", &aliases, None);

    // Task with start date tomorrow
    let has_start = Task::new("Task ^tomorrow", &aliases, None);

    // Without "!" - tasks with no date should be filtered out
    assert!(!no_start.matches_search_term("^>today"));
    assert!(has_start.matches_search_term("^>today"));

    // With "!" - tasks with no date should pass the filter
    assert!(no_start.matches_search_term("^>today!"));
    assert!(has_start.matches_search_term("^>today!"));
}

#[test]
fn test_not_set_operator_with_due_date() {
    let aliases = std::collections::HashMap::new();
    let now = Local::now().date_naive();
    // Dynamic future date to ensure test stability
    let future_year = now.year() + 2;
    let filter = format!("@<{}-01-01", future_year);
    let filter_not_set = format!("{}!", filter);

    // Task with no due date
    let no_due = Task::new("Task without due", &aliases, None);

    // Task with due date tomorrow
    let has_due = Task::new("Task @tomorrow", &aliases, None);

    // Without "!" - tasks with no date should be filtered out
    assert!(!no_due.matches_search_term(&filter));
    assert!(has_due.matches_search_term(&filter));

    // With "!" - tasks with no date should pass the filter
    assert!(no_due.matches_search_term(&filter_not_set));
    assert!(has_due.matches_search_term(&filter_not_set));
}

#[test]
fn test_is_ready_token_consumed() {
    let aliases = std::collections::HashMap::new();

    // Create a task that doesn't contain the text "is:ready"
    let task = Task::new("Work on project", &aliases, None);

    // The is:ready filter should be consumed and not fail text search
    assert!(task.matches_search_term("is:ready"));
}

#[test]
fn test_is_ready_filters_future_start_dates() {
    // Create a TestContext and TaskStore with it
    let ctx = Arc::new(TestContext::new());
    let mut store = TaskStore::new(ctx.clone());

    let aliases = std::collections::HashMap::new();
    // Use UTC to avoid timezone issues where "yesterday" in local time is still "today" in UTC
    let now = chrono::Utc::now().date_naive();

    // Task with future start date
    let mut future = Task::new(
        &format!(
            "Future Task ^{}",
            (now + Duration::days(5)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );
    future.calendar_href = "cal1".to_string();

    // Task with past start date (2 days ago to avoid timezone edge cases)
    let mut past = Task::new(
        &format!(
            "Past Task ^{}",
            (now - Duration::days(2)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );
    past.calendar_href = "cal1".to_string();

    // Task with no start date
    let mut no_start = Task::new("No Start Task", &aliases, None);
    no_start.calendar_href = "cal1".to_string();

    store.add_task(future);
    store.add_task(past);
    store.add_task(no_start);

    let options = FilterOptions {
        active_cal_href: None,
        hidden_calendars: &HashSet::new(),
        selected_categories: &HashSet::new(),
        selected_locations: &HashSet::new(),
        match_all_categories: false,
        search_term: "is:ready",
        hide_completed_global: true,
        hide_fully_completed_tags: false,
        hide_aliases_in_sidebar: false,
        cutoff_date: None,
        min_duration: None,
        max_duration: None,
        include_unset_duration: true,
        urgent_days: 7,
        urgent_prio: 5,
        default_priority: 5,
        start_grace_period_days: 1,
        sort_standard_by_priority: false,
        expanded_done_groups: &HashSet::new(),
        expanded_tags: &HashSet::new(),
        expanded_locations: &HashSet::new(),

        max_done_roots: usize::MAX,
        max_done_subtasks: usize::MAX,
        tag_aliases: &HashMap::new(),
    };

    let filtered = store.filter(options).items;

    // Future task should be filtered out
    assert!(!filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Future")
        } else {
            false
        }
    }));

    // Past task and no start task should be included
    assert!(filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Past")
        } else {
            false
        }
    }));
    assert!(filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("No Start")
        } else {
            false
        }
    }));
}

#[test]
fn test_is_ready_filters_blocked_tasks() {
    // Create a TestContext and TaskStore with it
    let ctx = Arc::new(TestContext::new());
    let mut store = TaskStore::new(ctx.clone());

    let aliases = std::collections::HashMap::new();

    // Create a dependency task that is not done
    let mut dep_task = Task::new("Dependency Task", &aliases, None);
    dep_task.status = TaskStatus::NeedsAction;
    dep_task.calendar_href = "cal1".to_string();

    // Create a task that depends on the first
    let mut blocked_task = Task::new("Blocked Task", &aliases, None);
    blocked_task.dependencies.push(dep_task.uid.clone());
    blocked_task.calendar_href = "cal1".to_string();

    // Create an unblocked task
    let mut unblocked_task = Task::new("Unblocked Task", &aliases, None);
    unblocked_task.calendar_href = "cal1".to_string();

    store.add_task(dep_task);
    store.add_task(blocked_task);
    store.add_task(unblocked_task);

    let options = FilterOptions {
        active_cal_href: None,
        hidden_calendars: &HashSet::new(),
        selected_categories: &HashSet::new(),
        selected_locations: &HashSet::new(),
        match_all_categories: false,
        search_term: "is:ready",
        hide_completed_global: true,
        hide_fully_completed_tags: false,
        hide_aliases_in_sidebar: false,
        cutoff_date: None,
        min_duration: None,
        max_duration: None,
        include_unset_duration: true,
        urgent_days: 7,
        urgent_prio: 5,
        default_priority: 5,
        start_grace_period_days: 1,
        sort_standard_by_priority: false,
        expanded_done_groups: &HashSet::new(),
        expanded_tags: &HashSet::new(),
        expanded_locations: &HashSet::new(),

        max_done_roots: usize::MAX,
        max_done_subtasks: usize::MAX,
        tag_aliases: &HashMap::new(),
    };

    let filtered = store.filter(options).items;

    // Blocked task should be filtered out
    assert!(!filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Blocked")
        } else {
            false
        }
    }));

    // Unblocked and dependency tasks should be included
    assert!(filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Unblocked")
        } else {
            false
        }
    }));
    assert!(filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Dependency")
        } else {
            false
        }
    }));
}

#[test]
fn test_is_ready_combines_with_other_filters() {
    // Create a TestContext and TaskStore with it
    let ctx = Arc::new(TestContext::new());
    let mut store = TaskStore::new(ctx.clone());

    let aliases = std::collections::HashMap::new();
    let now = Local::now().date_naive();

    // Ready task with #work tag
    let mut work_task = Task::new("Work Task #work", &aliases, None);
    work_task.calendar_href = "cal1".to_string();

    // Ready task with #personal tag
    let mut personal_task = Task::new("Personal Task #personal", &aliases, None);
    personal_task.calendar_href = "cal1".to_string();

    // Future task with #work tag (not ready)
    let mut future_work = Task::new(
        &format!(
            "Future Work #work ^{}",
            (now + Duration::days(5)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );
    future_work.calendar_href = "cal1".to_string();

    store.add_task(work_task);
    store.add_task(personal_task);
    store.add_task(future_work);

    let options = FilterOptions {
        active_cal_href: None,
        hidden_calendars: &HashSet::new(),
        selected_categories: &HashSet::new(),
        selected_locations: &HashSet::new(),
        match_all_categories: false,
        search_term: "is:ready #work",
        hide_completed_global: true,
        hide_fully_completed_tags: false,
        hide_aliases_in_sidebar: false,
        cutoff_date: None,
        min_duration: None,
        max_duration: None,
        include_unset_duration: true,
        urgent_days: 7,
        urgent_prio: 5,
        default_priority: 5,
        start_grace_period_days: 1,
        sort_standard_by_priority: false,
        expanded_done_groups: &HashSet::new(),
        expanded_tags: &HashSet::new(),
        expanded_locations: &HashSet::new(),

        max_done_roots: usize::MAX,
        max_done_subtasks: usize::MAX,
        tag_aliases: &HashMap::new(),
    };

    let filtered = store.filter(options).items;

    // Only the ready work task should be included
    assert!(filtered.len() == 1);
    if let cfait::store::TaskListItem::Task(task) = &filtered[0] {
        assert!(task.summary.contains("Work Task"));
        assert!(!task.summary.contains("Future"));
        assert!(!task.summary.contains("Personal"));
    } else {
        panic!("Expected Task variant");
    }
}

#[test]
fn test_is_ready_filters_implicitly_future_tasks() {
    // Test that a child task with no start date but with a parent that has a future start date
    // is filtered out by is:ready (is_implicitly_future behavior)
    let ctx = Arc::new(TestContext::new());
    let mut store = TaskStore::new(ctx.clone());

    let aliases = std::collections::HashMap::new();
    // Use UTC to avoid timezone issues
    let now = chrono::Utc::now().date_naive();

    // Parent task with future start date
    let mut parent = Task::new(
        &format!(
            "Parent Project ^{}",
            (now + Duration::days(5)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );
    parent.calendar_href = "cal1".to_string();

    // Child task with no start date - should inherit parent's future start
    let mut child = Task::new("Child Task", &aliases, None);
    child.parent_uid = Some(parent.uid.clone());
    child.calendar_href = "cal1".to_string();

    // Another task with no parent and no start date - should be ready
    let mut ready_task = Task::new("Ready Task", &aliases, None);
    ready_task.calendar_href = "cal1".to_string();

    store.add_task(parent);
    store.add_task(child);
    store.add_task(ready_task);

    let options = FilterOptions {
        active_cal_href: None,
        hidden_calendars: &HashSet::new(),
        selected_categories: &HashSet::new(),
        selected_locations: &HashSet::new(),
        match_all_categories: false,
        search_term: "is:ready",
        hide_completed_global: true,
        hide_fully_completed_tags: false,
        hide_aliases_in_sidebar: false,
        cutoff_date: None,
        min_duration: None,
        max_duration: None,
        include_unset_duration: true,
        urgent_days: 7,
        urgent_prio: 5,
        default_priority: 5,
        sort_standard_by_priority: false,
        start_grace_period_days: 1,
        expanded_done_groups: &HashSet::new(),
        expanded_tags: &HashSet::new(),
        expanded_locations: &HashSet::new(),
        max_done_roots: usize::MAX,
        max_done_subtasks: usize::MAX,
        tag_aliases: &HashMap::new(),
    };

    let filtered = store.filter(options).items;

    // Child task should be filtered out (implicitly future via parent)
    assert!(!filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Child Task")
        } else {
            false
        }
    }));

    // Ready task should be included
    assert!(filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Ready Task")
        } else {
            false
        }
    }));

    // Parent task should also be filtered out (explicit future start)
    assert!(!filtered.iter().any(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            task.summary.contains("Parent Project")
        } else {
            false
        }
    }));

    // Verify is_implicitly_future flag is set correctly on filtered tasks
    // We need to filter without is:ready to get the child task
    let options_all = FilterOptions {
        active_cal_href: None,
        hidden_calendars: &HashSet::new(),
        selected_categories: &HashSet::new(),
        selected_locations: &HashSet::new(),
        match_all_categories: false,
        search_term: "",
        hide_completed_global: true,
        hide_fully_completed_tags: false,
        hide_aliases_in_sidebar: false,
        cutoff_date: None,
        min_duration: None,
        max_duration: None,
        include_unset_duration: true,
        urgent_days: 7,
        urgent_prio: 5,
        default_priority: 5,
        sort_standard_by_priority: false,
        start_grace_period_days: 1,
        expanded_done_groups: &HashSet::new(),
        expanded_tags: &HashSet::new(),
        expanded_locations: &HashSet::new(),
        max_done_roots: usize::MAX,
        max_done_subtasks: usize::MAX,
        tag_aliases: &HashMap::new(),
    };

    let all_tasks = store.filter(options_all).items;
    let child_task = all_tasks.iter().find_map(|t| {
        if let cfait::store::TaskListItem::Task(task) = t {
            if task.summary.contains("Child Task") {
                Some(task)
            } else {
                None
            }
        } else {
            None
        }
    });

    assert!(child_task.is_some(), "Child task should exist in all tasks");
    if let Some(child) = child_task {
        // Child has no explicit start date, so is_future_start should be false
        assert!(
            !child.is_future_start,
            "Child should not have explicit future start"
        );
        // But it should have is_implicitly_future set to true due to parent
        assert!(
            child.is_implicitly_future,
            "Child should be implicitly future via parent"
        );
    }
}

#[test]
fn test_random_selector_skips_implicitly_future_tasks() {
    // Test that the random task selector skips tasks that are implicitly future via parent
    use cfait::store::select_weighted_random_index;

    let aliases = std::collections::HashMap::new();
    let now = chrono::Utc::now().date_naive();

    // Parent task with future start date
    let mut parent = Task::new(
        &format!(
            "Parent Project ^{}",
            (now + Duration::days(5)).format("%Y-%m-%d")
        ),
        &aliases,
        None,
    );
    parent.calendar_href = "cal1".to_string();
    // Set the transient flag that would be set by the filter pipeline
    parent.is_future_start = true;
    parent.is_implicitly_future = false;

    // Child task with no start date - should inherit parent's future start
    let mut child = Task::new("Child Task", &aliases, None);
    child.parent_uid = Some(parent.uid.clone());
    child.calendar_href = "cal1".to_string();
    // Set the transient flags that would be set by the filter pipeline
    child.is_future_start = false;
    child.is_implicitly_future = true; // This is what we're testing

    // Ready task with no parent and no start date
    let mut ready_task = Task::new("Ready Task", &aliases, None);
    ready_task.calendar_href = "cal1".to_string();
    ready_task.is_future_start = false;
    ready_task.is_implicitly_future = false;

    let tasks = vec![parent, child, ready_task];

    // Run the selector multiple times - it should never pick the child or parent
    let mut ready_count = 0;
    let iterations = 100;
    for _ in 0..iterations {
        if let Some(idx) = select_weighted_random_index(&tasks, 5) {
            assert_ne!(
                idx, 0,
                "Should not select parent task (explicit future start)"
            );
            assert_ne!(idx, 1, "Should not select child task (implicitly future)");
            assert_eq!(idx, 2, "Should only select ready task");
            ready_count += 1;
        }
    }

    assert_eq!(
        ready_count, iterations,
        "Should have selected ready task every time"
    );
}
