//! Pure git porcelain output parsers for `service::git_ops`.
//!
//! These functions parse the text output of `git status --porcelain=v1`
//! and similar commands. No I/O: they take `&str` and return structured data.

/// Parse `git status --porcelain=v1 -u` output into three file lists.
///
/// Returns `(staged, unstaged, untracked)` where each entry is the file path
/// string from the porcelain line.
#[allow(clippy::string_slice)] // line[3..]: git porcelain format has 2 ASCII status chars + ASCII space
pub fn parse_git_status_porcelain(stdout: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
    let mut staged: Vec<String> = Vec::new();
    let mut unstaged: Vec<String> = Vec::new();
    let mut untracked: Vec<String> = Vec::new();

    for line in stdout.lines() {
        if line.len() < 3 {
            continue;
        }
        let x = line.chars().next().unwrap_or(' ');
        let y = line.chars().nth(1).unwrap_or(' ');
        let path = line[3..].to_string();

        match (x, y) {
            ('?', '?') => untracked.push(path),
            (' ', _) => unstaged.push(path),
            (_, ' ') => staged.push(path),
            _ => {
                // Both staged and unstaged modifications.
                staged.push(path.clone());
                unstaged.push(path);
            }
        }
    }

    (staged, unstaged, untracked)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn parse_empty_output_returns_empty_vecs() {
        let (s, u, t) = parse_git_status_porcelain("");
        assert!(s.is_empty());
        assert!(u.is_empty());
        assert!(t.is_empty());
    }

    #[test]
    fn parse_untracked_file() {
        let output = "?? new_file.txt\n";
        let (staged, unstaged, untracked) = parse_git_status_porcelain(output);
        assert!(staged.is_empty());
        assert!(unstaged.is_empty());
        assert_eq!(untracked, vec!["new_file.txt"]);
    }

    #[test]
    fn parse_staged_file() {
        let output = "A  staged.txt\n";
        let (staged, unstaged, untracked) = parse_git_status_porcelain(output);
        assert_eq!(staged, vec!["staged.txt"]);
        assert!(unstaged.is_empty());
        assert!(untracked.is_empty());
    }

    #[test]
    fn parse_unstaged_modified_file() {
        // 'M' in second column = unstaged modification
        let output = " M modified.txt\n";
        let (staged, unstaged, untracked) = parse_git_status_porcelain(output);
        assert!(staged.is_empty());
        assert_eq!(unstaged, vec!["modified.txt"]);
        assert!(untracked.is_empty());
    }

    #[test]
    fn parse_both_staged_and_unstaged() {
        // 'MM' = staged + unstaged in same file
        let output = "MM both.txt\n";
        let (staged, unstaged, untracked) = parse_git_status_porcelain(output);
        assert_eq!(staged, vec!["both.txt"]);
        assert_eq!(unstaged, vec!["both.txt"]);
        assert!(untracked.is_empty());
    }

    #[test]
    fn parse_mixed_output() {
        let output = "A  staged.rs\n M unstaged.rs\n?? untracked.rs\n";
        let (staged, unstaged, untracked) = parse_git_status_porcelain(output);
        assert_eq!(staged, vec!["staged.rs"]);
        assert_eq!(unstaged, vec!["unstaged.rs"]);
        assert_eq!(untracked, vec!["untracked.rs"]);
    }

    #[test]
    fn parse_skips_short_lines() {
        // Lines shorter than 3 chars should be skipped.
        let output = "A \nMM both.txt\n";
        let (staged, _unstaged, _untracked) = parse_git_status_porcelain(output);
        // Only "both.txt" should appear — the 2-char "A " line is skipped.
        assert_eq!(staged, vec!["both.txt"]);
    }
}
