//! Native task progress parsing for tasks.md files.
//!
//! This module provides native parsing of task checkboxes in markdown files,
//! supporting both bullet lists (`- [ ]`) and numbered lists (`1. [ ]`).

use crate::error::{OrchestratorError, Result};
use crate::tui::log_deduplicator;
use regex::Regex;
use std::fmt::Write as _;
use std::path::Path;
use std::sync::OnceLock;
use tracing::debug;

/// Task progress information.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TaskProgress {
    /// Number of completed tasks.
    pub completed: u32,
    /// Total number of tasks.
    pub total: u32,
}

impl TaskProgress {
    /// Create a new TaskProgress with zero counts.
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a TaskProgress with specific counts.
    #[cfg(test)]
    pub fn with_counts(completed: u32, total: u32) -> Self {
        Self { completed, total }
    }
}

/// Get the task checkbox regex pattern.
///
/// Pattern matches both bullet and numbered lists with checkboxes:
/// - `- [ ] Task` (bullet unchecked)
/// - `- [x] Task` (bullet checked)
/// - `* [X] Task` (asterisk checked)
/// - `1. [ ] Task` (numbered unchecked)
/// - `10. [x] Task` (numbered checked)
///
/// Does NOT match:
/// - `  - [ ] Sub-item` (indented sub-bullets)
/// - `Some text [ ]` (inline checkboxes)
/// - `## [x] Header` (markdown headers)
fn task_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        // ^: start of line
        // (?:[-*]|\d+\.): bullet (-/*) or numbered (digits followed by .)
        // \s+: one or more whitespace
        // \[([ xX])\]: checkbox with capture group for status
        Regex::new(r"^(?:[-*]|\d+\.)\s+\[([ xX])\]").expect("Invalid regex pattern")
    })
}

/// Parse task progress from markdown content.
///
/// Parses each line looking for task checkboxes at the start of lines.
/// Returns the count of completed and total tasks.
/// When change_id is provided, emits deduplicated debug logs.
pub fn parse_content(content: &str, change_id: Option<&str>) -> TaskProgress {
    let regex = task_regex();
    let mut progress = TaskProgress::new();

    for line in content.lines() {
        if let Some(captures) = regex.captures(line) {
            progress.total += 1;
            // Capture group 1 contains the checkbox status: ' ', 'x', or 'X'
            if let Some(status) = captures.get(1) {
                let status_char = status.as_str();
                if status_char == "x" || status_char == "X" {
                    progress.completed += 1;
                }
            }
        }
    }

    if let Some(change_id) = change_id {
        if log_deduplicator::should_log_task_progress(change_id, progress.completed, progress.total)
        {
            debug!(
                "Parsed task progress: {}/{} tasks completed",
                progress.completed, progress.total
            );
        }
    }

    progress
}

/// Parse task progress from a file.
///
/// Reads the file content and parses it for task checkboxes.
/// When change_id is provided, emits deduplicated debug logs.
pub fn parse_file(path: &Path, change_id: Option<&str>) -> Result<TaskProgress> {
    let content = std::fs::read_to_string(path).map_err(|e| {
        OrchestratorError::ConfigLoad(format!("Failed to read tasks file {:?}: {}", path, e))
    })?;

    Ok(parse_content(&content, change_id))
}

/// Parse task progress for a change by its ID.
///
/// Looks for tasks.md at `openspec/changes/{change_id}/tasks.md`.
pub fn parse_change(change_id: &str) -> Result<TaskProgress> {
    let tasks_path = Path::new("openspec/changes")
        .join(change_id)
        .join("tasks.md");

    if !tasks_path.exists() {
        return Err(OrchestratorError::ConfigLoad(format!(
            "Tasks file not found: {:?}",
            tasks_path
        )));
    }

    parse_file(&tasks_path, Some(change_id))
}

/// Parse task progress with worktree priority and base tree fallback.
///
/// Resolution order:
/// 1. Try worktree_path/openspec/changes/{change_id}/tasks.md (uncommitted)
/// 2. Fallback to openspec/changes/{change_id}/tasks.md (base tree)
///
/// This function is designed for TUI auto-refresh to read the latest progress
/// from worktrees where AI agents update tasks.md before committing.
///
/// # Deprecated
/// Use [`parse_progress_with_fallback`] instead, which provides comprehensive
/// fallback order: worktree → archive → base.
#[deprecated(
    since = "0.3.0",
    note = "Use parse_progress_with_fallback for comprehensive fallback order"
)]
#[allow(dead_code)]
pub fn parse_change_with_worktree_fallback(
    change_id: &str,
    worktree_path: Option<&Path>,
) -> Result<TaskProgress> {
    // Try worktree first (uncommitted changes)
    if let Some(wt_path) = worktree_path {
        let wt_tasks = wt_path
            .join("openspec/changes")
            .join(change_id)
            .join("tasks.md");

        if wt_tasks.exists() {
            debug!("Reading tasks from worktree: {:?}", wt_tasks);
            return parse_file(&wt_tasks, Some(change_id));
        }
    }

    // Fallback to base tree
    parse_change(change_id)
}

/// Find the archive directory entry for a change.
///
/// Searches for an archive directory matching either:
/// - `{change_id}` - Simple archive
/// - `{date}-{change_id}` - Date-prefixed archive (e.g., `2024-01-15-add-feature`)
///
/// Returns the path to the archive directory if found.
fn find_archive_directory(change_id: &str, base_path: Option<&Path>) -> Option<std::path::PathBuf> {
    let archive_dir = match base_path {
        Some(base) => base.join("openspec/changes/archive"),
        None => Path::new("openspec/changes/archive").to_path_buf(),
    };

    if !archive_dir.exists() {
        return None;
    }

    // Try exact match first
    let exact_match = archive_dir.join(change_id);
    if exact_match.exists() && exact_match.is_dir() {
        return Some(exact_match);
    }

    // Try date-prefixed match
    if let Ok(entries) = std::fs::read_dir(&archive_dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let name = entry.file_name();
            let name_str = name.to_string_lossy();
            // Check if it ends with "-{change_id}" and is a directory
            if name_str.ends_with(&format!("-{}", change_id)) && entry.path().is_dir() {
                return Some(entry.path());
            }
        }
    }

    None
}

/// Parse task progress from the archive directory.
///
/// Looks for tasks.md at `openspec/changes/archive/{change_id}/tasks.md` or
/// `openspec/changes/archive/{date}-{change_id}/tasks.md` (date-prefixed format).
/// This is used to retrieve final progress for archived changes when
/// the change is no longer in the active changes directory.
///
/// # Deprecated
/// Use [`parse_progress_with_fallback`] instead, which provides comprehensive
/// fallback order: worktree → archive → base.
#[deprecated(
    since = "0.3.0",
    note = "Use parse_progress_with_fallback for comprehensive fallback order"
)]
#[allow(dead_code)]
pub fn parse_archived_change(change_id: &str) -> Result<TaskProgress> {
    let archive_path = find_archive_directory(change_id, None).ok_or_else(|| {
        OrchestratorError::ConfigLoad(format!(
            "Archived directory not found for change '{}' in openspec/changes/archive/",
            change_id
        ))
    })?;

    let tasks_path = archive_path.join("tasks.md");

    if !tasks_path.exists() {
        return Err(OrchestratorError::ConfigLoad(format!(
            "Archived tasks file not found: {:?}",
            tasks_path
        )));
    }

    parse_file(&tasks_path, Some(change_id))
}

/// Parse task progress with worktree priority for archived changes.
///
/// Resolution order when worktree_path is provided:
/// 1. Try worktree_path/openspec/changes/archive/{change_id}/tasks.md or date-prefixed (archived in worktree)
/// 2. Try worktree_path/openspec/changes/{change_id}/tasks.md (not yet archived in worktree)
/// 3. Fallback to openspec/changes/archive/{change_id}/tasks.md or date-prefixed (base tree)
///
/// Resolution order when worktree_path is None:
/// 1. Try openspec/changes/archive/{change_id}/tasks.md or date-prefixed (base tree)
///
/// This function is designed for Archived/Merged changes in TUI auto-refresh to read
/// the latest progress from worktrees where the archive may not yet be committed.
///
/// # Deprecated
/// Use [`parse_progress_with_fallback`] instead, which provides comprehensive
/// fallback order: worktree → archive → base.
#[deprecated(
    since = "0.3.0",
    note = "Use parse_progress_with_fallback for comprehensive fallback order"
)]
#[allow(dead_code)]
#[allow(deprecated)]
pub fn parse_archived_change_with_worktree_fallback(
    change_id: &str,
    worktree_path: Option<&Path>,
) -> Result<TaskProgress> {
    // Try worktree first (uncommitted archive or pre-archive state)
    if let Some(wt_path) = worktree_path {
        // Try archived location in worktree (supports date-prefixed directories)
        if let Some(archive_path) = find_archive_directory(change_id, Some(wt_path)) {
            let wt_archive_tasks = archive_path.join("tasks.md");
            if wt_archive_tasks.exists() {
                debug!(
                    "Reading archived tasks from worktree archive: {:?}",
                    wt_archive_tasks
                );
                return parse_file(&wt_archive_tasks, Some(change_id));
            }
        }

        // Try active location in worktree (archive not yet committed)
        let wt_tasks = wt_path
            .join("openspec/changes")
            .join(change_id)
            .join("tasks.md");

        if wt_tasks.exists() {
            debug!(
                "Reading archived tasks from worktree (pre-archive): {:?}",
                wt_tasks
            );
            return parse_file(&wt_tasks, Some(change_id));
        }
    }

    // Fallback to base tree archive (supports date-prefixed directories)
    parse_archived_change(change_id)
}

/// Parse task progress with comprehensive fallback order: worktree → archive → base.
///
/// This is the unified progress retrieval helper used by both TUI and Web components.
///
/// Resolution order:
/// 1. Try worktree active location: worktree_path/openspec/changes/{change_id}/tasks.md
/// 2. Try worktree archive location: worktree_path/openspec/changes/archive/{change_id}/tasks.md (or date-prefixed)
/// 3. Try base tree archive location: openspec/changes/archive/{change_id}/tasks.md (or date-prefixed)
/// 4. Try base tree active location: openspec/changes/{change_id}/tasks.md
///
/// # Arguments
/// * `change_id` - The ID of the change to retrieve progress for
/// * `worktree_path` - Optional path to the worktree (for uncommitted changes)
///
/// # Returns
/// Task progress with completed and total counts, or an error if not found in any location.
///
/// # Example
/// ```ignore
/// use std::path::Path;
/// use conflux::task_parser;
///
/// // Without worktree (base tree only)
/// let progress = task_parser::parse_progress_with_fallback("my-change", None)?;
///
/// // With worktree (prioritizes worktree)
/// let wt_path = Path::new("/path/to/worktree");
/// let progress = task_parser::parse_progress_with_fallback("my-change", Some(wt_path))?;
/// # Ok::<(), conflux::error::OrchestratorError>(())
/// ```
pub fn parse_progress_with_fallback(
    change_id: &str,
    worktree_path: Option<&Path>,
) -> Result<TaskProgress> {
    // 1. Try worktree active location first (most recent uncommitted changes)
    if let Some(wt_path) = worktree_path {
        let wt_active = wt_path
            .join("openspec/changes")
            .join(change_id)
            .join("tasks.md");

        if wt_active.exists() {
            debug!(
                "Reading progress from worktree active location: {:?}",
                wt_active
            );
            return parse_file(&wt_active, Some(change_id));
        }

        // 2. Try worktree archive location (archived but not yet committed)
        if let Some(archive_path) = find_archive_directory(change_id, Some(wt_path)) {
            let wt_archive_tasks = archive_path.join("tasks.md");
            if wt_archive_tasks.exists() {
                debug!(
                    "Reading progress from worktree archive location: {:?}",
                    wt_archive_tasks
                );
                return parse_file(&wt_archive_tasks, Some(change_id));
            }
        }
    }

    // 3. Try base tree archive location (committed archive)
    if let Some(archive_path) = find_archive_directory(change_id, None) {
        let base_archive_tasks = archive_path.join("tasks.md");
        if base_archive_tasks.exists() {
            debug!(
                "Reading progress from base tree archive location: {:?}",
                base_archive_tasks
            );
            return parse_file(&base_archive_tasks, Some(change_id));
        }
    }

    // 4. Fallback to base tree active location (committed active change)
    let base_active = Path::new("openspec/changes")
        .join(change_id)
        .join("tasks.md");

    if base_active.exists() {
        debug!(
            "Reading progress from base tree active location: {:?}",
            base_active
        );
        return parse_file(&base_active, Some(change_id));
    }

    // Not found in any location
    Err(OrchestratorError::ConfigLoad(format!(
        "Tasks file not found for change '{}' in any location (worktree, archive, or base tree)",
        change_id
    )))
}

fn acceptance_follow_up_heading(attempt: u32) -> String {
    format!("## Acceptance #{} Failure Follow-up", attempt)
}

pub fn record_acceptance_follow_up(
    tasks_path: &Path,
    attempt: u32,
    findings: &[String],
) -> Result<()> {
    let mut content = std::fs::read_to_string(tasks_path).map_err(|e| {
        OrchestratorError::ConfigLoad(format!("Failed to read tasks file {:?}: {}", tasks_path, e))
    })?;

    let heading = acceptance_follow_up_heading(attempt);
    let mut normalized_findings = findings
        .iter()
        .map(|finding| finding.trim())
        .filter(|finding| !finding.is_empty())
        .map(str::to_string)
        .collect::<Vec<_>>();
    normalized_findings.sort();
    normalized_findings.dedup();

    if normalized_findings.is_empty() {
        normalized_findings
            .push("Investigate acceptance failure and apply the required fix".to_string());
    }

    if let Some(section_start) = content.find(&heading) {
        let section_body_start = content[section_start..]
            .find('\n')
            .map(|offset| section_start + offset + 1)
            .unwrap_or(content.len());
        let section_end = content[section_body_start..]
            .find("\n## ")
            .map(|offset| section_body_start + offset + 1)
            .unwrap_or(content.len());
        let existing_section = &content[section_body_start..section_end];
        let mut new_section = String::new();
        for finding in normalized_findings {
            let _ = writeln!(&mut new_section, "- [ ] {}", finding);
        }
        if existing_section.trim() != new_section.trim() {
            content.replace_range(section_body_start..section_end, &new_section);
        }
    } else {
        if !content.ends_with('\n') {
            content.push('\n');
        }
        if !content.ends_with("\n\n") {
            content.push('\n');
        }
        let _ = writeln!(&mut content, "{}", heading);
        for finding in normalized_findings {
            let _ = writeln!(&mut content, "- [ ] {}", finding);
        }
    }

    std::fs::write(tasks_path, content).map_err(|e| {
        OrchestratorError::ConfigLoad(format!(
            "Failed to write tasks file {:?}: {}",
            tasks_path, e
        ))
    })?;

    Ok(())
}

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

    // ====================
    // Bullet list format tests
    // ====================

    #[test]
    fn test_bullet_unchecked() {
        let content = "- [ ] Task 1\n- [ ] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_bullet_checked_lowercase() {
        let content = "- [x] Task 1\n- [x] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 2);
    }

    #[test]
    fn test_bullet_checked_uppercase() {
        let content = "- [X] Task 1\n- [X] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 2);
    }

    #[test]
    fn test_asterisk_bullets() {
        let content = "* [ ] Task 1\n* [x] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 1);
    }

    #[test]
    fn test_bullet_mixed_status() {
        let content = "- [x] Completed\n- [ ] Pending\n- [X] Also done";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 3);
        assert_eq!(progress.completed, 2);
    }

    // ====================
    // Numbered list format tests
    // ====================

    #[test]
    fn test_numbered_unchecked() {
        let content = "1. [ ] Task 1\n2. [ ] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_record_acceptance_follow_up_appends_unchecked_tasks() {
        let dir = tempfile::tempdir().unwrap();
        let tasks_path = dir.path().join("tasks.md");
        std::fs::write(&tasks_path, "## Implementation Tasks\n- [x] done\n").unwrap();

        record_acceptance_follow_up(
            &tasks_path,
            2,
            &[
                "missing repository coverage".to_string(),
                "add notification links".to_string(),
            ],
        )
        .unwrap();

        let content = std::fs::read_to_string(&tasks_path).unwrap();
        assert!(content.contains("## Acceptance #2 Failure Follow-up"));
        assert!(content.contains("- [ ] missing repository coverage"));
        assert!(content.contains("- [ ] add notification links"));

        let progress = parse_file(&tasks_path, None).unwrap();
        assert_eq!(progress.completed, 1);
        assert_eq!(progress.total, 3);
    }

    #[test]
    fn test_record_acceptance_follow_up_replaces_existing_section() {
        let dir = tempfile::tempdir().unwrap();
        let tasks_path = dir.path().join("tasks.md");
        std::fs::write(
            &tasks_path,
            "## Implementation Tasks\n- [x] done\n\n## Acceptance #1 Failure Follow-up\n- [x] stale\n",
        )
        .unwrap();

        record_acceptance_follow_up(&tasks_path, 1, &["fresh finding".to_string()]).unwrap();

        let content = std::fs::read_to_string(&tasks_path).unwrap();
        assert!(content.contains("## Acceptance #1 Failure Follow-up"));
        assert!(content.contains("- [ ] fresh finding"));
        assert!(!content.contains("- [x] stale"));
    }

    #[test]
    fn test_numbered_checked() {
        let content = "1. [x] Task 1\n2. [x] Task 2";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 2);
        assert_eq!(progress.completed, 2);
    }

    #[test]
    fn test_numbered_multi_digit() {
        let content = "1. [x] Task 1\n10. [ ] Task 10\n100. [X] Task 100";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 3);
        assert_eq!(progress.completed, 2);
    }

    #[test]
    fn test_numbered_mixed_status() {
        let content = "1. [x] Done\n2. [ ] Not done\n3. [X] Also done";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 3);
        assert_eq!(progress.completed, 2);
    }

    // ====================
    // Mixed format tests
    // ====================

    #[test]
    fn test_mixed_bullets_and_numbers() {
        let content =
            "- [x] Bullet done\n1. [ ] Number pending\n* [X] Asterisk done\n2. [x] Number done";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 4);
        assert_eq!(progress.completed, 3);
    }

    #[test]
    fn test_mixed_with_sections() {
        let content = r#"# Tasks

## Implementation
- [x] Task 1
- [ ] Task 2

## Testing
1. [x] Test 1
2. [ ] Test 2
"#;
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 4);
        assert_eq!(progress.completed, 2);
    }

    // ====================
    // Edge case tests
    // ====================

    #[test]
    fn test_empty_content() {
        let progress = parse_content("", None);
        assert_eq!(progress.total, 0);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_no_tasks() {
        let content = "# Just a header\nSome text without tasks.\n\n- Regular list item";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 0);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_indented_not_counted() {
        let content =
            "- [x] Parent task\n  - [ ] Sub-task (should not count)\n  - [x] Another sub-task";
        let progress = parse_content(content, None);
        // Only the parent task at the start of line should count
        assert_eq!(progress.total, 1);
        assert_eq!(progress.completed, 1);
    }

    #[test]
    fn test_inline_checkbox_not_counted() {
        let content = "Some text with [ ] inline checkbox\nAnother line [x] here";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 0);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_header_checkbox_not_counted() {
        let content = "## [x] Header with checkbox\n### [ ] Another header";
        let progress = parse_content(content, None);
        assert_eq!(progress.total, 0);
        assert_eq!(progress.completed, 0);
    }

    #[test]
    fn test_real_world_example() {
        let content = r#"# Tasks

## Implementation Tasks

- [x] Create `src/task_parser.rs` module with regex-based task parsing
- [x] Implement `TaskProgress` struct with `completed` and `total` fields
- [ ] Implement `parse_content()` function to parse task markdown content
- [ ] Implement `parse_file()` function to read and parse tasks.md files

## Testing Tasks

1. [ ] Add unit tests for bullet list format
2. [ ] Add unit tests for numbered list format
3. [x] Add unit tests for mixed format

## Validation

- [ ] Run `cargo test` to verify all tests pass
- [ ] Run `cargo clippy` to check for warnings
"#;
        let progress = parse_content(content, None);
        // 4 bullets + 3 numbered + 2 bullets = 9 total
        // 2 checked bullets + 1 checked numbered = 3 completed
        assert_eq!(progress.total, 9);
        assert_eq!(progress.completed, 3);
    }

    // ====================
    // TaskProgress struct tests
    // ====================

    #[test]
    fn test_task_progress_new() {
        let progress = TaskProgress::new();
        assert_eq!(progress.completed, 0);
        assert_eq!(progress.total, 0);
    }

    #[test]
    fn test_task_progress_with_counts() {
        let progress = TaskProgress::with_counts(5, 10);
        assert_eq!(progress.completed, 5);
        assert_eq!(progress.total, 10);
    }

    #[test]
    fn test_task_progress_default() {
        let progress = TaskProgress::default();
        assert_eq!(progress.completed, 0);
        assert_eq!(progress.total, 0);
    }

    // ====================
    // File parsing tests
    // ====================

    #[test]
    fn test_parse_file_not_found() {
        let result = parse_file(Path::new("/nonexistent/path/tasks.md"), None);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_change_not_found() {
        let result = parse_change("nonexistent-change-id");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_change_with_worktree_fallback_from_worktree() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let worktree_path = temp_dir.path();

        // Create worktree structure
        let change_dir = worktree_path.join("openspec/changes/test-change");
        std::fs::create_dir_all(&change_dir).unwrap();

        // Write tasks.md in worktree
        let tasks_content = "- [x] Task 1\n- [x] Task 2\n- [ ] Task 3";
        std::fs::write(change_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse with worktree
        let result = parse_progress_with_fallback("test-change", Some(worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 3);
    }

    // NOTE: These tests change the process cwd; serialize across the whole crate.

    #[test]
    fn test_parse_change_with_worktree_fallback_to_base() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create base tree structure
        let change_dir = base_path.join("openspec/changes/test-change");
        std::fs::create_dir_all(&change_dir).unwrap();

        // Write tasks.md in base tree
        let tasks_content = "- [x] Task 1\n- [ ] Task 2";
        std::fs::write(change_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse with non-existent worktree (should fallback to base)
        let result = parse_progress_with_fallback("test-change", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 1);
        assert_eq!(progress.total, 2);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    // ====================
    // Archived change parsing tests
    // ====================

    #[test]
    fn test_parse_archived_change_with_worktree_fallback_from_worktree_archive() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let worktree_path = temp_dir.path();

        // Create worktree archive structure
        let archive_dir = worktree_path.join("openspec/changes/archive/test-archived");
        std::fs::create_dir_all(&archive_dir).unwrap();

        // Write tasks.md in worktree archive
        let tasks_content = "- [x] Task 1\n- [x] Task 2\n- [x] Task 3\n- [ ] Task 4";
        std::fs::write(archive_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse with worktree
        let result = parse_progress_with_fallback("test-archived", Some(worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 3);
        assert_eq!(progress.total, 4);
    }

    #[test]
    fn test_parse_archived_change_with_worktree_fallback_from_worktree_active() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let worktree_path = temp_dir.path();

        // Create worktree active change structure (not yet archived in worktree)
        let change_dir = worktree_path.join("openspec/changes/test-prearchive");
        std::fs::create_dir_all(&change_dir).unwrap();

        // Write tasks.md in worktree active location
        let tasks_content = "- [x] Task 1\n- [x] Task 2\n- [ ] Task 3";
        std::fs::write(change_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse with worktree (should find in active location since not in archive yet)
        let result = parse_progress_with_fallback("test-prearchive", Some(worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 3);
    }

    #[test]
    fn test_parse_archived_change_with_worktree_fallback_to_base() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create base tree archive structure
        let archive_dir = base_path.join("openspec/changes/archive/test-base-archive");
        std::fs::create_dir_all(&archive_dir).unwrap();

        // Write tasks.md in base tree archive
        let tasks_content = "- [x] Task 1\n- [ ] Task 2";
        std::fs::write(archive_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse with non-existent worktree (should fallback to base)
        let result = parse_progress_with_fallback("test-base-archive", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 1);
        assert_eq!(progress.total, 2);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_archived_change_with_worktree_fallback_priority() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create both base archive and worktree archive
        let base_archive = base_path.join("openspec/changes/archive/test-priority");
        std::fs::create_dir_all(&base_archive).unwrap();
        std::fs::write(base_archive.join("tasks.md"), "- [ ] Old task").unwrap();

        let worktree_path = base_path.join("worktree");
        let wt_archive = worktree_path.join("openspec/changes/archive/test-priority");
        std::fs::create_dir_all(&wt_archive).unwrap();
        std::fs::write(
            wt_archive.join("tasks.md"),
            "- [x] New task 1\n- [x] New task 2",
        )
        .unwrap();

        // Parse with worktree (should prefer worktree over base)
        let result = parse_progress_with_fallback("test-priority", Some(&worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 2);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    // ====================
    // Date-prefixed archive tests
    // ====================

    #[test]
    fn test_parse_archived_change_date_prefixed() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create date-prefixed archive directory
        let archive_dir = base_path.join("openspec/changes/archive/2024-01-15-test-change");
        std::fs::create_dir_all(&archive_dir).unwrap();

        // Write tasks.md in date-prefixed archive
        let tasks_content = "- [x] Task 1\n- [x] Task 2\n- [ ] Task 3";
        std::fs::write(archive_dir.join("tasks.md"), tasks_content).unwrap();

        // Parse should find the date-prefixed directory
        let result = parse_progress_with_fallback("test-change", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 3);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_archived_change_exact_match_preferred() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create both exact match and date-prefixed archive
        let exact_archive = base_path.join("openspec/changes/archive/test-exact");
        std::fs::create_dir_all(&exact_archive).unwrap();
        std::fs::write(exact_archive.join("tasks.md"), "- [x] Exact task").unwrap();

        let date_archive = base_path.join("openspec/changes/archive/2024-01-15-test-exact");
        std::fs::create_dir_all(&date_archive).unwrap();
        std::fs::write(date_archive.join("tasks.md"), "- [ ] Date task").unwrap();

        // Parse should prefer exact match over date-prefixed
        let result = parse_progress_with_fallback("test-exact", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 1);
        assert_eq!(progress.total, 1);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_archived_change_with_worktree_fallback_date_prefixed() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create date-prefixed archive in base tree
        let base_archive = base_path.join("openspec/changes/archive/2026-01-17-test-date");
        std::fs::create_dir_all(&base_archive).unwrap();
        std::fs::write(
            base_archive.join("tasks.md"),
            "- [x] Task 1\n- [x] Task 2\n- [x] Task 3",
        )
        .unwrap();

        // Parse without worktree (should find date-prefixed archive)
        let result = parse_progress_with_fallback("test-date", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 3);
        assert_eq!(progress.total, 3);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_find_archive_directory_not_found() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Create archive directory but no matching entries
        let archive_dir = base_path.join("openspec/changes/archive");
        std::fs::create_dir_all(&archive_dir).unwrap();

        // Should return None when no match found
        let result = find_archive_directory("nonexistent", Some(base_path));
        assert!(result.is_none());
    }

    #[test]
    fn test_find_archive_directory_exact_match() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Create exact match archive
        let exact_archive = base_path.join("openspec/changes/archive/exact-match");
        std::fs::create_dir_all(&exact_archive).unwrap();

        let result = find_archive_directory("exact-match", Some(base_path));
        assert!(result.is_some());
        assert_eq!(result.unwrap(), exact_archive);
    }

    #[test]
    fn test_find_archive_directory_date_prefixed() {
        use tempfile::TempDir;

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Create date-prefixed archive
        let date_archive = base_path.join("openspec/changes/archive/2024-01-15-my-feature");
        std::fs::create_dir_all(&date_archive).unwrap();

        let result = find_archive_directory("my-feature", Some(base_path));
        assert!(result.is_some());
        assert_eq!(result.unwrap(), date_archive);
    }

    // ====================
    // Unified fallback helper tests
    // ====================

    #[test]
    fn test_parse_progress_with_fallback_worktree_active() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create worktree active location
        let worktree_path = base_path.join("worktree");
        let wt_active = worktree_path.join("openspec/changes/test-fallback");
        std::fs::create_dir_all(&wt_active).unwrap();
        std::fs::write(
            wt_active.join("tasks.md"),
            "- [x] Task 1\n- [x] Task 2\n- [ ] Task 3",
        )
        .unwrap();

        // Parse should find worktree active location first
        let result = parse_progress_with_fallback("test-fallback", Some(&worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 3);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_progress_with_fallback_worktree_archive() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create worktree archive location (no active location)
        let worktree_path = base_path.join("worktree");
        let wt_archive = worktree_path.join("openspec/changes/archive/test-wt-archive");
        std::fs::create_dir_all(&wt_archive).unwrap();
        std::fs::write(
            wt_archive.join("tasks.md"),
            "- [x] Task 1\n- [x] Task 2\n- [x] Task 3\n- [ ] Task 4",
        )
        .unwrap();

        // Parse should find worktree archive location
        let result = parse_progress_with_fallback("test-wt-archive", Some(&worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 3);
        assert_eq!(progress.total, 4);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_progress_with_fallback_base_archive() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create base tree archive location (no worktree)
        let base_archive = base_path.join("openspec/changes/archive/test-base-archive");
        std::fs::create_dir_all(&base_archive).unwrap();
        std::fs::write(base_archive.join("tasks.md"), "- [x] Task 1\n- [x] Task 2").unwrap();

        // Parse should find base tree archive location
        let result = parse_progress_with_fallback("test-base-archive", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 2);
        assert_eq!(progress.total, 2);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_progress_with_fallback_base_active() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create base tree active location (no archive, no worktree)
        let base_active = base_path.join("openspec/changes/test-base-active");
        std::fs::create_dir_all(&base_active).unwrap();
        std::fs::write(base_active.join("tasks.md"), "- [ ] Task 1").unwrap();

        // Parse should find base tree active location
        let result = parse_progress_with_fallback("test-base-active", None);
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 0);
        assert_eq!(progress.total, 1);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_progress_with_fallback_priority_order() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Create all locations with different progress values
        let worktree_path = base_path.join("worktree");

        // Worktree active (highest priority)
        let wt_active = worktree_path.join("openspec/changes/test-priority");
        std::fs::create_dir_all(&wt_active).unwrap();
        std::fs::write(
            wt_active.join("tasks.md"),
            "- [x] WT Active 1\n- [x] WT Active 2\n- [x] WT Active 3",
        )
        .unwrap();

        // Worktree archive
        let wt_archive = worktree_path.join("openspec/changes/archive/test-priority");
        std::fs::create_dir_all(&wt_archive).unwrap();
        std::fs::write(
            wt_archive.join("tasks.md"),
            "- [x] WT Archive 1\n- [x] WT Archive 2",
        )
        .unwrap();

        // Base archive
        let base_archive = base_path.join("openspec/changes/archive/test-priority");
        std::fs::create_dir_all(&base_archive).unwrap();
        std::fs::write(base_archive.join("tasks.md"), "- [x] Base Archive 1").unwrap();

        // Base active (lowest priority)
        let base_active = base_path.join("openspec/changes/test-priority");
        std::fs::create_dir_all(&base_active).unwrap();
        std::fs::write(base_active.join("tasks.md"), "- [ ] Base Active 1").unwrap();

        // Parse should find worktree active location (highest priority)
        let result = parse_progress_with_fallback("test-priority", Some(&worktree_path));
        assert!(result.is_ok());
        let progress = result.unwrap();
        assert_eq!(progress.completed, 3); // From worktree active
        assert_eq!(progress.total, 3);

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }

    #[test]
    fn test_parse_progress_with_fallback_not_found() {
        use std::env;
        use tempfile::TempDir;

        // Acquire lock to prevent concurrent directory changes
        let _lock = crate::test_support::cwd_lock().lock().unwrap();

        let temp_dir = TempDir::new().unwrap();
        let base_path = temp_dir.path();

        // Change to temp directory
        let original_dir = env::current_dir().unwrap();
        env::set_current_dir(base_path).unwrap();

        // Don't create any locations
        let result = parse_progress_with_fallback("nonexistent", None);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("not found for change 'nonexistent'"));

        // Restore directory
        env::set_current_dir(original_dir).unwrap();
    }
}
