use super::*;
use crate::model::{DiffLineKind, FileOperation, FileState};

#[test]
fn test_parse_log_record() {
    // Tab-separated fields
    let record = "abc12345\tdef67890\tuser@example.com\t2024-01-29T15:30:00+0900\tInitial commit\ttrue\tfalse\tmain,feature";
    let change = Parser::parse_log_record(record).unwrap();

    assert_eq!(change.change_id, "abc12345");
    assert_eq!(change.commit_id, "def67890");
    assert_eq!(change.author, "user@example.com");
    assert_eq!(change.description, "Initial commit");
    assert!(change.is_working_copy);
    assert!(!change.is_empty);
    assert_eq!(change.bookmarks, vec!["main", "feature"]);
}

#[test]
fn test_parse_log_record_no_bookmarks() {
    let record =
        "abc12345\tdef67890\tuser@example.com\t2024-01-29T15:30:00+0900\tTest\tfalse\ttrue\t";
    let change = Parser::parse_log_record(record).unwrap();

    assert!(change.bookmarks.is_empty());
    assert!(change.is_empty);
}

#[test]
fn test_parse_log_record_empty_fields() {
    // Root commit has empty author and description
    let record = "zzzzzzzz\t00000000\t\t1970-01-01T00:00:00+0000\t\tfalse\ttrue\t";
    let change = Parser::parse_log_record(record).unwrap();

    assert_eq!(change.change_id, "zzzzzzzz");
    assert_eq!(change.commit_id, "00000000");
    assert_eq!(change.author, "");
    assert_eq!(change.description, "");
    assert!(!change.is_working_copy);
    assert!(change.is_empty);
}

#[test]
fn test_parse_log_multiple_records() {
    let output = "abc12345\tdef67890\tuser@example.com\t2024-01-29T15:30:00+0900\tFirst\ttrue\tfalse\t\n\
                  xyz98765\tuvw43210\tother@example.com\t2024-01-28T10:00:00+0900\tSecond\tfalse\tfalse\t\n";

    let changes = Parser::parse_log(output).unwrap();
    assert_eq!(changes.len(), 2);
    assert_eq!(changes[0].description, "First");
    assert_eq!(changes[1].description, "Second");
}

#[test]
fn test_parse_status_line_added() {
    let file = Parser::parse_status_line("A new_file.rs").unwrap();
    assert_eq!(file.path, "new_file.rs");
    assert!(matches!(file.state, FileState::Added));
}

#[test]
fn test_parse_status_line_modified() {
    let file = Parser::parse_status_line("M src/main.rs").unwrap();
    assert_eq!(file.path, "src/main.rs");
    assert!(matches!(file.state, FileState::Modified));
}

#[test]
fn test_parse_status_line_deleted() {
    let file = Parser::parse_status_line("D old_file.txt").unwrap();
    assert_eq!(file.path, "old_file.txt");
    assert!(matches!(file.state, FileState::Deleted));
}

#[test]
fn test_parse_status_line_conflicted() {
    let file = Parser::parse_status_line("C conflicted.rs").unwrap();
    assert_eq!(file.path, "conflicted.rs");
    assert!(matches!(file.state, FileState::Conflicted));
}

#[test]
fn test_parse_status_output() {
    let output = r#"Working copy changes:
A new_file.rs
M src/main.rs
Working copy : abc12345 def67890 (empty) (no description set)
Parent commit: xyz98765 uvw43210 Initial commit"#;

    let status = Parser::parse_status(output).unwrap();
    assert_eq!(status.files.len(), 2);
    assert!(!status.has_conflicts);
    assert_eq!(status.working_copy_change_id, "abc12345");
    assert_eq!(status.parent_change_id, "xyz98765");
}

#[test]
fn test_parse_status_with_conflict() {
    let output = r#"Working copy changes:
C conflicted.rs
Working copy : abc12345 def67890 description
Parent commit: xyz98765 uvw43210 parent"#;

    let status = Parser::parse_status(output).unwrap();
    assert!(status.has_conflicts);
    assert_eq!(status.files.len(), 1);
    assert!(matches!(status.files[0].state, FileState::Conflicted));
}

#[test]
fn test_parse_status_with_markers() {
    // jj 0.37+ format with (@) and (@-) markers
    let output = r#"Working copy changes:
A new_file.rs
M src/main.rs
Working copy  (@) : abc12345 def67890 (empty) (no description set)
Parent commit (@-): xyz98765 uvw43210 Initial commit"#;

    let status = Parser::parse_status(output).unwrap();
    assert_eq!(status.files.len(), 2);
    assert_eq!(status.working_copy_change_id, "abc12345");
    assert_eq!(status.parent_change_id, "xyz98765");
}

#[test]
fn test_parse_status_line_renamed() {
    // jj format: "R prefix{old => new}"
    let file = Parser::parse_status_line("R src/{old_name.rs => new_name.rs}").unwrap();
    assert_eq!(file.path, "src/new_name.rs");
    match file.state {
        FileState::Renamed { from } => assert_eq!(from, "src/old_name.rs"),
        _ => panic!("Expected Renamed state"),
    }
}

#[test]
fn test_parse_status_line_renamed_no_prefix() {
    // jj format without common prefix
    let file = Parser::parse_status_line("R {old.rs => new.rs}").unwrap();
    assert_eq!(file.path, "new.rs");
    match file.state {
        FileState::Renamed { from } => assert_eq!(from, "old.rs"),
        _ => panic!("Expected Renamed state"),
    }
}

#[test]
fn test_parse_status_line_renamed_deep_path() {
    // Nested path
    let file = Parser::parse_status_line("R src/ui/views/{status.rs => status_view.rs}").unwrap();
    assert_eq!(file.path, "src/ui/views/status_view.rs");
    match file.state {
        FileState::Renamed { from } => assert_eq!(from, "src/ui/views/status.rs"),
        _ => panic!("Expected Renamed state"),
    }
}

// =========================================================================
// parse_show tests
// =========================================================================

#[test]
fn test_parse_show_header() {
    let output = r#"Commit ID: abc123def456
Change ID: xyz789uvw012
Author   : Test User <test@example.com> (2024-01-30 12:00:00)
Committer: Test User <test@example.com> (2024-01-30 12:00:00)

    Add new feature
"#;
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.commit_id, "abc123def456");
    assert_eq!(content.author, "Test User <test@example.com>");
    assert_eq!(content.timestamp, "2024-01-30 12:00:00");
    assert_eq!(content.description, "Add new feature");
}

#[test]
fn test_parse_show_with_bookmarks_and_tags_keeps_description() {
    // Regression: `Bookmarks:`/`Tags:` header lines (present for bookmarked/
    // tagged changes) used to be misread as end-of-header, dropping the
    // description in color-words mode (stat/git were unaffected).
    let output = r#"Commit ID: 2d31c7f1e79247270ce3822a9b6b15561ebdd2cb
Change ID: xqnktzmlworukplnyrropmtzylsuxxlv
Bookmarks: main main@git main@origin
Tags     : v0.6.1
Author   : Test <test@example.com> (2026-06-05 13:56:39)
Committer: Test <test@example.com> (2026-06-05 14:56:53)

    feat: stack diff via palette (jj 0.42 multi-revision show)

    Body line of the description.

Modified regular file src/main.rs:
   10   10:     fn main() {
"#;
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(
        content.description,
        "feat: stack diff via palette (jj 0.42 multi-revision show)\n\nBody line of the description."
    );
    assert_eq!(content.file_count(), 1);
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
}

#[test]
fn test_parse_show_stack_bookmarked_change_keeps_description() {
    // Same regression via the stack parser (ChangeHeader label was showing
    // "(no description)" for the bookmarked block)
    let output = r#"Commit ID: abc123def456
Change ID: xqnktzmlworukplnyrropmtzylsuxxlv
Bookmarks: main
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    Bookmarked change
"#;
    let content = Parser::parse_show_stack(output).unwrap();

    assert_eq!(content.description, "xqnktzml Bookmarked change");
    let header = content
        .lines
        .iter()
        .find(|l| l.kind == DiffLineKind::ChangeHeader)
        .unwrap();
    assert_eq!(header.content, "◉ xqnktzml Bookmarked change");
}

#[test]
fn test_parse_show_empty_no_changes() {
    let output = r#"Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    (no description set)
"#;
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.commit_id, "abc123");
    assert!(!content.has_changes());
    assert_eq!(content.file_count(), 0);
}

#[test]
fn test_parse_show_with_file_diff() {
    let output = r#"Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    Fix bug

Modified regular file src/main.rs:
   10   10:     fn main() {
   11     : -       println!("old");
        11: +       println!("new");
   12   12:     }
"#;
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.commit_id, "abc123");
    assert_eq!(content.description, "Fix bug");
    assert!(content.has_changes());
    assert_eq!(content.file_count(), 1);

    // Check file header
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
    assert_eq!(content.lines[0].content, "src/main.rs");
}

#[test]
fn test_parse_show_multiple_files() {
    let output = r#"Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    Add files

Added regular file src/new.rs:
    1: + pub fn hello() {}

Modified regular file src/lib.rs:
   10   10: mod existing;
        11: + mod new;
"#;
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.file_count(), 2);

    // First file
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
    assert_eq!(content.lines[0].content, "src/new.rs");

    // Separator before second file
    let sep_pos = content
        .lines
        .iter()
        .position(|l| l.kind == DiffLineKind::Separator)
        .unwrap();
    assert!(sep_pos > 0);

    // Second file header
    let second_header = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .nth(1)
        .unwrap();
    assert_eq!(second_header.content, "src/lib.rs");
}

#[test]
fn test_parse_show_conflict_diff() {
    let output = "Commit ID: c285b17e
Change ID: lqxuvokn
Author   : Test <test@example.com> (2026-02-05 10:33:31)
Committer: Test <test@example.com> (2026-02-05 10:33:50)

    branch B

Created conflict in test.txt:
   1     : branch A content
        1: <<<<<<< conflict 1 of 1
        2: %%%%%%% changes from initial
        3: -line 1
        4: +branch A content
        5: +++++++ branch B
        6: branch B content
        7: >>>>>>> conflict 1 of 1 ends
";
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.commit_id, "c285b17e");
    assert_eq!(content.description, "branch B");
    assert!(content.has_changes());
    assert_eq!(content.file_count(), 1);

    // File header is "test.txt"
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
    assert_eq!(content.lines[0].content, "test.txt");

    // Diff lines were parsed (conflict markers appear as content)
    assert!(content.lines.len() > 1);
}

// =========================================================================
// parse_show_stack tests (multi-revision jj show, jj 0.42+)
// =========================================================================

#[test]
fn test_parse_show_stack_two_changes() {
    // jj show <revset> output: newest first, one block per revision
    let output = r#"Commit ID: abc123def456
Change ID: xqnktzmlworukplnyrropmtzylsuxxlv
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    Newest change

Modified regular file src/main.rs:
   10   10:     fn main() {
        11: +       println!("new");
Commit ID: 789012abcdef
Change ID: ztrqnqttynpltxovptprnvnvlpymqmym
Author   : Test <test@example.com> (2026-06-04 12:00:00)
Committer: Test <test@example.com> (2026-06-04 12:00:00)

    Older change

Added regular file src/lib.rs:
    1: + pub fn hello() {}
"#;
    let content = Parser::parse_show_stack(output).unwrap();

    // Combined metadata comes from the first (newest) block
    assert_eq!(content.commit_id, "abc123def456");
    assert_eq!(content.author, "Test <test@example.com>");

    // Description is a per-change summary (change_short + first desc line)
    assert_eq!(
        content.description,
        "xqnktzml Newest change\nztrqnqtt Older change"
    );

    // One ChangeHeader per revision
    let change_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::ChangeHeader)
        .collect();
    assert_eq!(change_headers.len(), 2);
    assert_eq!(change_headers[0].content, "◉ xqnktzml Newest change");
    assert_eq!(change_headers[1].content, "◉ ztrqnqtt Older change");

    // File headers from both blocks survive (existing [/] file nav works)
    assert_eq!(content.file_count(), 2);

    // First line is the newest change's boundary header
    assert_eq!(content.lines[0].kind, DiffLineKind::ChangeHeader);
}

#[test]
fn test_parse_show_stack_single_change_fallback() {
    // Fallback case: revset resolved to just the selected change
    let output = r#"Commit ID: abc123
Change ID: xyz789uvw012
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    Only change

Modified regular file src/main.rs:
   10   10:     fn main() {
"#;
    let content = Parser::parse_show_stack(output).unwrap();

    assert_eq!(content.commit_id, "abc123");
    assert_eq!(content.description, "xyz789uv Only change");
    assert_eq!(
        content
            .lines
            .iter()
            .filter(|l| l.kind == DiffLineKind::ChangeHeader)
            .count(),
        1
    );
    assert_eq!(content.file_count(), 1);
}

#[test]
fn test_parse_show_stack_no_description() {
    let output = r#"Commit ID: abc123
Change ID: xyz789uvw012
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    (no description set)
"#;
    let content = Parser::parse_show_stack(output).unwrap();

    // jj prints "(no description set)" as the description placeholder
    assert_eq!(content.description, "xyz789uv (no description set)");
    assert!(!content.has_changes());
}

#[test]
fn test_parse_show_stack_empty_output() {
    let content = Parser::parse_show_stack("").unwrap();
    assert!(content.lines.is_empty());
    assert_eq!(content.description, "");
}

#[test]
fn test_parse_show_stack_stat_two_changes() {
    // jj show <revset> --stat: header + histogram body per block
    let output = r#"Commit ID: abc123def456
Change ID: xqnktzmlworukplnyrropmtzylsuxxlv
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    Newest change

src/main.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Commit ID: 789012abcdef
Change ID: ztrqnqttynpltxovptprnvnvlpymqmym
Author   : Test <test@example.com> (2026-06-04 12:00:00)
Committer: Test <test@example.com> (2026-06-04 12:00:00)

    Older change

src/lib.rs | 1 +
1 file changed, 1 insertion(+), 0 deletions(-)
"#;
    let content = Parser::parse_show_stack_stat(output).unwrap();

    assert_eq!(content.commit_id, "abc123def456");
    assert_eq!(
        content.description,
        "xqnktzml Newest change\nztrqnqtt Older change"
    );

    let change_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::ChangeHeader)
        .collect();
    assert_eq!(change_headers.len(), 2);
    assert_eq!(change_headers[0].content, "◉ xqnktzml Newest change");

    // Stat body lines survive as plain Context lines for both blocks
    assert!(
        content
            .lines
            .iter()
            .any(|l| l.kind == DiffLineKind::Context && l.content.contains("src/main.rs"))
    );
    assert!(
        content
            .lines
            .iter()
            .any(|l| l.kind == DiffLineKind::Context && l.content.contains("src/lib.rs"))
    );
}

#[test]
fn test_parse_show_stack_git_two_changes() {
    // jj show <revset> --git: header + git unified diff per block
    let output = r#"Commit ID: abc123def456
Change ID: xqnktzmlworukplnyrropmtzylsuxxlv
Author   : Test <test@example.com> (2026-06-05 12:00:00)
Committer: Test <test@example.com> (2026-06-05 12:00:00)

    Newest change

diff --git a/src/main.rs b/src/main.rs
index 1111111111..2222222222 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1 +1,1 @@
-old line
+new line
Commit ID: 789012abcdef
Change ID: ztrqnqttynpltxovptprnvnvlpymqmym
Author   : Test <test@example.com> (2026-06-04 12:00:00)
Committer: Test <test@example.com> (2026-06-04 12:00:00)

    Older change

diff --git a/src/lib.rs b/src/lib.rs
index 3333333333..4444444444 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -0,0 +1,1 @@
+pub fn hello() {}
"#;
    let content = Parser::parse_show_stack_git(output).unwrap();

    assert_eq!(content.commit_id, "abc123def456");
    assert_eq!(
        content.description,
        "xqnktzml Newest change\nztrqnqtt Older change"
    );

    let change_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::ChangeHeader)
        .collect();
    assert_eq!(change_headers.len(), 2);

    // FileHeaders from both blocks (git `diff --git` lines)
    let file_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .collect();
    assert_eq!(file_headers.len(), 2);
    assert_eq!(file_headers[0].content, "src/main.rs");
    assert_eq!(file_headers[1].content, "src/lib.rs");

    // Added/Deleted lines parsed across blocks
    assert!(
        content
            .lines
            .iter()
            .any(|l| l.kind == DiffLineKind::Added && l.content == "new line")
    );
    assert!(
        content
            .lines
            .iter()
            .any(|l| l.kind == DiffLineKind::Deleted && l.content == "old line")
    );
    assert!(
        content
            .lines
            .iter()
            .any(|l| l.kind == DiffLineKind::Added && l.content == "pub fn hello() {}")
    );
}

#[test]
fn test_extract_file_info() {
    let (path, op) = Parser::extract_file_info("Modified regular file src/main.rs:").unwrap();
    assert_eq!(path, "src/main.rs");
    assert_eq!(op, FileOperation::Modified);

    let (path, op) = Parser::extract_file_info("Added regular file src/new.rs:").unwrap();
    assert_eq!(path, "src/new.rs");
    assert_eq!(op, FileOperation::Added);

    let (path, op) = Parser::extract_file_info("Removed regular file old.txt:").unwrap();
    assert_eq!(path, "old.txt");
    assert_eq!(op, FileOperation::Deleted);

    let (path, op) = Parser::extract_file_info("Created conflict in test.txt:").unwrap();
    assert_eq!(path, "test.txt");
    assert_eq!(op, FileOperation::Modified);

    let (path, op) = Parser::extract_file_info("Resolved conflict in test.txt:").unwrap();
    assert_eq!(path, "test.txt");
    assert_eq!(op, FileOperation::Modified);

    assert!(Parser::extract_file_info("Some other line").is_none());
}

#[test]
fn test_parse_author_line() {
    let (author, ts) =
        Parser::parse_author_line("Test User <test@example.com> (2024-01-30 12:00:00)").unwrap();
    assert_eq!(author, "Test User <test@example.com>");
    assert_eq!(ts, "2024-01-30 12:00:00");
}

#[test]
fn test_parse_diff_line_context() {
    let line =
        Parser::parse_diff_line("   10   10:     fn main() {", FileOperation::Modified).unwrap();
    assert_eq!(line.kind, DiffLineKind::Context);
    assert_eq!(line.line_numbers, Some((Some(10), Some(10))));
}

#[test]
fn test_parse_diff_line_added() {
    let line = Parser::parse_diff_line(
        "        11: +       println!(\"new\");",
        FileOperation::Modified,
    )
    .unwrap();
    assert_eq!(line.kind, DiffLineKind::Added);
}

#[test]
fn test_parse_diff_line_deleted() {
    let line = Parser::parse_diff_line(
        "   11     : -       println!(\"old\");",
        FileOperation::Modified,
    )
    .unwrap();
    assert_eq!(line.kind, DiffLineKind::Deleted);
}

#[test]
fn test_parse_diff_line_added_file_no_prefix() {
    // Lines in added files don't have + prefix
    let line = Parser::parse_diff_line("        1: // Hotfix", FileOperation::Added).unwrap();
    assert_eq!(line.kind, DiffLineKind::Added);
}

#[test]
fn test_parse_diff_line_deleted_file_no_prefix() {
    // Lines in deleted files don't have - prefix
    let line = Parser::parse_diff_line("    1    : old content", FileOperation::Deleted).unwrap();
    assert_eq!(line.kind, DiffLineKind::Deleted);
}

// =========================================================================
// Graph parsing tests (Phase 3.5)
// =========================================================================

#[test]
fn test_split_graph_prefix_simple() {
    let (prefix, id) = Parser::split_graph_prefix("@  oqwroxvu").unwrap();
    assert_eq!(prefix, "@  ");
    assert_eq!(id, "oqwroxvu");
}

#[test]
fn test_split_graph_prefix_one_level() {
    let (prefix, id) = Parser::split_graph_prefix("│ ○  nuzyqrpm").unwrap();
    assert_eq!(prefix, "│ ○  ");
    assert_eq!(id, "nuzyqrpm");
}

#[test]
fn test_split_graph_prefix_two_level() {
    let (prefix, id) = Parser::split_graph_prefix("│ │ ○  uslxsspn").unwrap();
    assert_eq!(prefix, "│ │ ○  ");
    assert_eq!(id, "uslxsspn");
}

#[test]
fn test_split_graph_prefix_complex() {
    let (prefix, id) = Parser::split_graph_prefix("│ ○ │  rnstomqt").unwrap();
    assert_eq!(prefix, "│ ○ │  ");
    assert_eq!(id, "rnstomqt");
}

#[test]
fn test_split_graph_prefix_no_change_id() {
    // Graph-only lines shouldn't reach this function (filtered by TAB check)
    // but if they do, it should error
    let result = Parser::split_graph_prefix("│ ├─╮");
    assert!(result.is_err());
}

#[test]
fn test_parse_log_with_graph_simple() {
    let output = "@  oqwroxvu\t1f7a8c00\tuser@example.com\t2026-01-30T16:17:51+0900\texperimental: results\ttrue\tfalse\t\n\
                  ○  vxvxrlkn\tdd9bda5a\tuser@example.com\t2026-01-30T16:17:51+0900\texperimental: try\tfalse\tfalse\t";

    let changes = Parser::parse_log(output).unwrap();
    assert_eq!(changes.len(), 2);

    assert_eq!(changes[0].graph_prefix, "@  ");
    assert_eq!(changes[0].change_id, "oqwroxvu");
    assert!(changes[0].is_working_copy);
    assert!(!changes[0].is_graph_only);

    assert_eq!(changes[1].graph_prefix, "○  ");
    assert_eq!(changes[1].change_id, "vxvxrlkn");
    assert!(!changes[1].is_working_copy);
}

#[test]
fn test_parse_log_with_graph_branch() {
    // Simulates a branch with graph-only line
    let output = "@  oqwroxvu\t1f7a8c00\tuser@example.com\t2026-01-30T16:17:51+0900\tfeature\ttrue\tfalse\t\n\
                  │ ○  nuzyqrpm\t8b644ab5\tuser@example.com\t2026-01-30T16:17:46+0900\tmain\tfalse\tfalse\t\n\
                  ├─╯\n\
                  ○  basecommit\tbase1234\tuser@example.com\t2026-01-30T16:15:24+0900\tbase\tfalse\tfalse\t";

    let changes = Parser::parse_log(output).unwrap();
    assert_eq!(changes.len(), 4);

    // First change
    assert_eq!(changes[0].graph_prefix, "@  ");
    assert_eq!(changes[0].change_id, "oqwroxvu");
    assert!(!changes[0].is_graph_only);

    // Second change (in branch)
    assert_eq!(changes[1].graph_prefix, "│ ○  ");
    assert_eq!(changes[1].change_id, "nuzyqrpm");
    assert!(!changes[1].is_graph_only);

    // Graph-only line (branch merge)
    assert_eq!(changes[2].graph_prefix, "├─╯");
    assert!(changes[2].is_graph_only);
    assert!(changes[2].change_id.is_empty());

    // Base commit
    assert_eq!(changes[3].graph_prefix, "○  ");
    assert_eq!(changes[3].change_id, "basecommit");
    assert!(!changes[3].is_graph_only);
}

#[test]
fn test_parse_log_graph_only_lines() {
    // Various graph-only patterns
    let patterns = ["│ ├─╮", "├─╯ │", "├───╯", "│ │", "│"];

    for pattern in patterns {
        let changes = Parser::parse_log(pattern).unwrap();
        assert_eq!(changes.len(), 1);
        assert!(changes[0].is_graph_only);
        assert_eq!(changes[0].graph_prefix, pattern);
    }
}

#[test]
fn test_parse_log_empty_lines_skipped() {
    let output = "@  oqwroxvu\t1f7a8c00\tuser@example.com\t2026-01-30T16:17:51+0900\ttest\ttrue\tfalse\t\n\
                  \n\
                  ○  vxvxrlkn\tdd9bda5a\tuser@example.com\t2026-01-30T16:17:51+0900\ttest2\tfalse\tfalse\t";

    let changes = Parser::parse_log(output).unwrap();
    assert_eq!(changes.len(), 2);
}

// =========================================================================
// parse_op_log tests
// =========================================================================

#[test]
fn test_parse_op_log_single() {
    // Tab-separated: id, user, timestamp, description
    let output = "abc123def456\tuser@example.com\t5 minutes ago\tsnapshot working copy";

    let operations = Parser::parse_op_log(output).unwrap();
    assert_eq!(operations.len(), 1);
    assert_eq!(operations[0].id, "abc123def456");
    assert_eq!(operations[0].user, "user@example.com");
    assert_eq!(operations[0].timestamp, "5 minutes ago");
    assert_eq!(operations[0].description, "snapshot working copy");
    assert!(operations[0].is_current); // First operation is current
}

#[test]
fn test_parse_op_log_multiple() {
    let output = "abc123def456\tuser@example.com\t5 minutes ago\tsnapshot working copy\n\
                  xyz789uvw012\tuser@example.com\t10 minutes ago\tdescribe commit abc\n\
                  def456ghi789\tuser@example.com\t1 hour ago\tnew empty commit";

    let operations = Parser::parse_op_log(output).unwrap();
    assert_eq!(operations.len(), 3);

    // First is current
    assert!(operations[0].is_current);
    assert!(!operations[1].is_current);
    assert!(!operations[2].is_current);

    // Check order preserved
    assert_eq!(operations[0].description, "snapshot working copy");
    assert_eq!(operations[1].description, "describe commit abc");
    assert_eq!(operations[2].description, "new empty commit");
}

#[test]
fn test_parse_op_log_empty_lines_skipped() {
    let output = "abc123\tuser\t5 min ago\top1\n\n\nxyz789\tuser\t10 min ago\top2";

    let operations = Parser::parse_op_log(output).unwrap();
    assert_eq!(operations.len(), 2);
}

#[test]
fn test_parse_op_log_malformed_lines_skipped() {
    // Lines with fewer than 4 tab-separated fields should be skipped
    let output = "abc123\tuser\t5 min ago\tvalid op\n\
                  malformed line\n\
                  xyz789\tuser\t10 min ago\tanother valid op";

    let operations = Parser::parse_op_log(output).unwrap();
    assert_eq!(operations.len(), 2);
}

// =========================================================================
// parse_file_annotate tests
// =========================================================================

#[test]
fn test_parse_file_annotate_basic() {
    // Format: "<change_id>\t<commit_id> <author> <timestamp>  <line_number>: <content>"
    let output = "twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19    1: //! Tij\n\
                  twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19    2: //!\n\
                  qplomrst\tbbb22222 taro 2026-01-28 15:22:00    3: mod app;";

    let content = Parser::parse_file_annotate(output, "src/main.rs").unwrap();
    assert_eq!(content.file_path, "src/main.rs");
    assert_eq!(content.len(), 3);

    // First line - first in hunk
    assert_eq!(content.lines[0].change_id, "twzksoxt");
    assert_eq!(content.lines[0].commit_id, "aaa11111");
    assert_eq!(content.lines[0].author, "nakamura");
    assert_eq!(content.lines[0].line_number, 1);
    assert!(content.lines[0].first_in_hunk);
    assert_eq!(content.lines[0].content, "//! Tij");

    // Second line - continuation (same change_id)
    assert_eq!(content.lines[1].change_id, "twzksoxt");
    assert_eq!(content.lines[1].commit_id, "aaa11111");
    assert_eq!(content.lines[1].line_number, 2);
    assert!(!content.lines[1].first_in_hunk);

    // Third line - new hunk (different change_id)
    assert_eq!(content.lines[2].change_id, "qplomrst");
    assert_eq!(content.lines[2].commit_id, "bbb22222");
    assert_eq!(content.lines[2].line_number, 3);
    assert!(content.lines[2].first_in_hunk);
}

#[test]
fn test_parse_file_annotate_with_tabs_in_content() {
    // Content contains tabs - should be handled correctly
    let output = "twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19    1: \tindented\twith\ttabs";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].content, "\tindented\twith\ttabs");
}

#[test]
fn test_parse_file_annotate_empty_content() {
    // Empty content after line number
    let output = "twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19    1:";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].content, "");
}

#[test]
fn test_parse_file_annotate_skips_empty_lines() {
    // Empty lines in output are skipped
    let output = "twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19    1: line1\n\n\nqplomrst\tbbb22222 taro 2026-01-28 15:22:00    4: line4";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 2);
    assert_eq!(content.lines[0].line_number, 1);
    assert_eq!(content.lines[1].line_number, 4); // Preserves original line numbers from jj
}

#[test]
fn test_parse_file_annotate_author_with_digits() {
    // Author name contains digits - should still parse correctly
    let output = "abc12345\tccc33333 user1 2026-01-30 10:43:19    1: some content";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].change_id, "abc12345");
    assert_eq!(content.lines[0].commit_id, "ccc33333");
    assert_eq!(content.lines[0].author, "user1");
    assert_eq!(content.lines[0].line_number, 1);
    assert_eq!(content.lines[0].content, "some content");
}

#[test]
fn test_parse_file_annotate_content_with_colon_pattern() {
    // Content contains patterns like "1: foo" - should not confuse parser
    let output = "twzksoxt\taaa11111 nakamura 2026-01-30 10:43:19   10: let x = 1: foo";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].line_number, 10);
    assert_eq!(content.lines[0].content, "let x = 1: foo");
}

#[test]
fn test_parse_file_annotate_variable_change_id_length() {
    // change_id longer than 8 chars (if jj config changes)
    let output = "abcdefghij\txyz1234567 user 2026-01-30 10:43:19    1: content";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].change_id, "abcdefghij");
    assert_eq!(content.lines[0].commit_id, "xyz1234567");

    // change_id and commit_id shorter than 8 chars
    let output3 = "abc\txyz user 2026-01-30 10:43:19    1: content";

    let content3 = Parser::parse_file_annotate(output3, "test.rs").unwrap();
    assert_eq!(content3.len(), 1);
    assert_eq!(content3.lines[0].change_id, "abc");
    assert_eq!(content3.lines[0].commit_id, "xyz");
}

#[test]
fn test_parse_file_annotate_complex_author_name() {
    // Author with spaces (full name) or special characters
    let output = "twzksoxt\taaa11111 John Doe 2026-01-30 10:43:19    1: content";

    let content = Parser::parse_file_annotate(output, "test.rs").unwrap();
    assert_eq!(content.len(), 1);
    assert_eq!(content.lines[0].author, "John Doe");
}

// =========================================================================
// parse_resolve_list tests (Phase 9)
// =========================================================================

#[test]
fn test_parse_resolve_list_tab_delimiter() {
    let output = "test.txt\t2-sided conflict\n";
    let files = Parser::parse_resolve_list(output);
    assert_eq!(files.len(), 1);
    assert_eq!(files[0].path, "test.txt");
    assert_eq!(files[0].description, "2-sided conflict");
}

#[test]
fn test_parse_resolve_list_space_delimiter() {
    // jj 0.37.x uses spaces (verified with xxd)
    let output = "test.txt    2-sided conflict\n";
    let files = Parser::parse_resolve_list(output);
    assert_eq!(files.len(), 1);
    assert_eq!(files[0].path, "test.txt");
    assert_eq!(files[0].description, "2-sided conflict");
}

#[test]
fn test_parse_resolve_list_multiple_spaces() {
    let output = "src/main.rs    2-sided conflict\nsrc/lib.rs     3-sided conflict\n";
    let files = Parser::parse_resolve_list(output);
    assert_eq!(files.len(), 2);
    assert_eq!(files[0].path, "src/main.rs");
    assert_eq!(files[0].description, "2-sided conflict");
    assert_eq!(files[1].path, "src/lib.rs");
    assert_eq!(files[1].description, "3-sided conflict");
}

#[test]
fn test_parse_resolve_list_path_with_spaces() {
    let output = "my file.txt    2-sided conflict\n";
    let files = Parser::parse_resolve_list(output);
    assert_eq!(files.len(), 1);
    assert_eq!(files[0].path, "my file.txt");
    assert_eq!(files[0].description, "2-sided conflict");
}

#[test]
fn test_parse_resolve_list_empty() {
    let output = "";
    let files = Parser::parse_resolve_list(output);
    assert!(files.is_empty());
}

// =========================================================================
// conflict field in log parser tests (Phase 9)
// =========================================================================

#[test]
fn test_parse_log_with_conflict() {
    let record = "abc12345\tdef67890\tuser@example.com\t2026-01-01T00:00:00+0900\tdescription\ttrue\tfalse\tmain\ttrue";
    let change = Parser::parse_log_record(record).unwrap();
    assert!(change.has_conflict);
}

#[test]
fn test_parse_log_without_conflict() {
    let record = "abc12345\tdef67890\tuser@example.com\t2026-01-01T00:00:00+0900\tdescription\ttrue\tfalse\tmain\tfalse";
    let change = Parser::parse_log_record(record).unwrap();
    assert!(!change.has_conflict);
}

#[test]
fn test_parse_log_missing_conflict_field() {
    // Backward compat: old format without conflict field
    let record = "abc12345\tdef67890\tuser@example.com\t2026-01-01T00:00:00+0900\tdescription\ttrue\tfalse\tmain";
    let change = Parser::parse_log_record(record).unwrap();
    assert!(!change.has_conflict); // defaults to false
}

// =========================================================================
// Multi-line description tests (parse_show)
// =========================================================================

#[test]
fn test_parse_show_multiline_description() {
    let output = "Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    test-2

    test-3

Modified regular file src/main.rs:
   10   10:     fn main() {
";
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.description, "test-2\n\ntest-3");
    assert_eq!(content.file_count(), 1);
}

#[test]
fn test_parse_show_multiline_description_no_diff() {
    // Empty commit with multi-line description (no file changes)
    let output = "Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    test-2

    test-3
";
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(content.description, "test-2\n\ntest-3");
    assert_eq!(content.file_count(), 0);
}

#[test]
fn test_parse_show_three_paragraph_description() {
    let output = "Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    Summary line

    Detailed paragraph one.

    Detailed paragraph two.

Modified regular file src/main.rs:
   10   10:     fn main() {
";
    let content = Parser::parse_show(output).unwrap();

    assert_eq!(
        content.description,
        "Summary line\n\nDetailed paragraph one.\n\nDetailed paragraph two."
    );
}

#[test]
fn test_parse_diff_body_single_file() {
    let output = "Modified regular file src/main.rs:
   10   10:     fn main() {
   11     : -       println!(\"old\");
        11: +       println!(\"new\");
   12   12:     }
";
    let content = Parser::parse_diff_body(output);

    // Should have no header info (it's a diff body, not jj show)
    assert!(content.commit_id.is_empty());
    assert!(content.author.is_empty());
    assert!(content.description.is_empty());

    // Should have file header + diff lines
    assert!(content.has_changes());
    assert_eq!(content.file_count(), 1);
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
    assert_eq!(content.lines[0].content, "src/main.rs");
}

#[test]
fn test_parse_diff_body_multiple_files() {
    let output = "Modified regular file src/main.rs:
   10   10:     fn main() {
   11     : -       println!(\"old\");
        11: +       println!(\"new\");
Added regular file src/new.rs:
        1: pub fn hello() {}
";
    let content = Parser::parse_diff_body(output);

    assert_eq!(content.file_count(), 2);
    // First file header
    assert_eq!(content.lines[0].kind, DiffLineKind::FileHeader);
    assert_eq!(content.lines[0].content, "src/main.rs");
}

#[test]
fn test_parse_diff_body_empty() {
    let content = Parser::parse_diff_body("");
    assert!(!content.has_changes());
    assert_eq!(content.file_count(), 0);
}

// =========================================================================
// Stat format parser tests
// =========================================================================

#[test]
fn test_parse_show_stat_with_header() {
    let output = "\
Commit ID: abc123
Change ID: xyz789
Author   : Test User <test@test.com> (2024-01-30 12:00:00)
Committer: Test User <test@test.com> (2024-01-30 12:00:00)

    Test commit message

src/main.rs | 10 ++++------
src/lib.rs  |  5 +++++
2 files changed, 9 insertions(+), 6 deletions(-)";

    let content = Parser::parse_show_stat(output).unwrap();
    assert_eq!(content.commit_id, "abc123");
    assert_eq!(content.description, "Test commit message");
    // Stat lines stored as Context (no line numbers)
    assert!(content.lines.len() >= 3);
    for line in &content.lines {
        assert_eq!(line.kind, DiffLineKind::Context);
        assert!(line.line_numbers.is_none());
    }
}

#[test]
fn test_parse_show_stat_empty_commit() {
    let output = "\
Commit ID: abc123
Change ID: xyz789
Author   : Test User <test@test.com> (2024-01-30 12:00:00)
Committer: Test User <test@test.com> (2024-01-30 12:00:00)

    Empty commit
";

    let content = Parser::parse_show_stat(output).unwrap();
    assert_eq!(content.lines.len(), 1);
    assert_eq!(content.lines[0].content, "(no changes)");
}

#[test]
fn test_parse_diff_body_stat() {
    let output = "\
src/main.rs | 10 ++++------
2 files changed, 9 insertions(+), 6 deletions(-)";

    let content = Parser::parse_diff_body_stat(output);
    assert_eq!(content.lines.len(), 2);
    assert!(content.lines[0].content.contains("src/main.rs"));
}

#[test]
fn test_parse_diff_body_stat_empty() {
    let content = Parser::parse_diff_body_stat("");
    assert_eq!(content.lines.len(), 1);
    assert_eq!(content.lines[0].content, "(no changes)");
}

// =========================================================================
// Git format parser tests
// =========================================================================

#[test]
fn test_parse_show_git_with_header() {
    let output = "\
Commit ID: abc123
Change ID: xyz789
Author   : Test User <test@test.com> (2024-01-30 12:00:00)
Committer: Test User <test@test.com> (2024-01-30 12:00:00)

    Test commit

diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
 fn main() {
+    println!(\"hello\");
-    println!(\"old\");
 }";

    let content = Parser::parse_show_git(output).unwrap();
    assert_eq!(content.commit_id, "abc123");
    assert_eq!(content.description, "Test commit");

    // Check file header
    let file_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .collect();
    assert_eq!(file_headers.len(), 1);
    assert_eq!(file_headers[0].content, "src/main.rs");

    // Check +/- lines
    let added: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::Added)
        .collect();
    assert_eq!(added.len(), 1);
    assert_eq!(added[0].content, "    println!(\"hello\");");

    let deleted: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::Deleted)
        .collect();
    assert_eq!(deleted.len(), 1);
    assert_eq!(deleted[0].content, "    println!(\"old\");");
}

#[test]
fn test_parse_git_triple_plus_not_added() {
    // +++ b/src/main.rs should NOT be classified as Added
    let output = "\
diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
 context
+added line
-deleted line";

    let content = Parser::parse_diff_body_git(output);

    // +++ should be skipped, not added
    let added: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::Added)
        .collect();
    assert_eq!(added.len(), 1);
    assert_eq!(added[0].content, "added line");

    // --- should be skipped, not deleted
    let deleted: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::Deleted)
        .collect();
    assert_eq!(deleted.len(), 1);
    assert_eq!(deleted[0].content, "deleted line");
}

#[test]
fn test_parse_git_hunk_header_is_context() {
    let output = "\
diff --git a/src/main.rs b/src/main.rs
@@ -1,3 +1,3 @@
 context";

    let content = Parser::parse_diff_body_git(output);
    let hunk_lines: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.content.starts_with("@@ "))
        .collect();
    assert_eq!(hunk_lines.len(), 1);
    assert_eq!(hunk_lines[0].kind, DiffLineKind::Context);
}

#[test]
fn test_parse_git_multiple_files() {
    let output = "\
diff --git a/src/main.rs b/src/main.rs
@@ -1,1 +1,1 @@
-old
+new
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,0 +1,1 @@
+added";

    let content = Parser::parse_diff_body_git(output);
    let file_headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .collect();
    assert_eq!(file_headers.len(), 2);
    assert_eq!(file_headers[0].content, "src/main.rs");
    assert_eq!(file_headers[1].content, "src/lib.rs");
}

#[test]
fn test_parse_git_index_line_skipped() {
    let output = "\
diff --git a/file.rs b/file.rs
index 1234567..abcdefg 100644
--- a/file.rs
+++ b/file.rs
@@ -1,1 +1,1 @@
+hello";

    let content = Parser::parse_diff_body_git(output);
    // index line should not appear in output
    let index_lines: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.content.starts_with("index "))
        .collect();
    assert_eq!(index_lines.len(), 0);
}

// =========================================================================
// file_op verification tests (v0.4.10 bug fix coverage)
// =========================================================================

#[test]
fn test_parse_show_file_op_set_on_headers() {
    let output = r#"Commit ID: abc123
Change ID: xyz789
Author   : Test <test@example.com> (2024-01-30 12:00:00)
Committer: Test <test@example.com> (2024-01-30 12:00:00)

    Multiple file types

Added regular file src/new.rs:
        1: pub fn new() {}
Modified regular file src/main.rs:
   10   10:     fn main() {
        11: +       println!("new");
   11   12:     }
Removed regular file old.txt:
    1    : old content
"#;
    let content = Parser::parse_show(output).unwrap();

    let headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .collect();
    assert_eq!(headers.len(), 3);

    assert_eq!(headers[0].content, "src/new.rs");
    assert_eq!(headers[0].file_op, Some(FileOperation::Added));

    assert_eq!(headers[1].content, "src/main.rs");
    assert_eq!(headers[1].file_op, Some(FileOperation::Modified));

    assert_eq!(headers[2].content, "old.txt");
    assert_eq!(headers[2].file_op, Some(FileOperation::Deleted));
}

#[test]
fn test_parse_diff_body_file_op_set_on_headers() {
    let output = "\
Added regular file src/new.rs:
        1: pub fn new() {}
Modified regular file src/main.rs:
   10   10:     fn main() {
        11: +       println!(\"new\");
   11   12:     }
Removed regular file old.txt:
    1    : old content
";
    let content = Parser::parse_diff_body(output);

    let headers: Vec<_> = content
        .lines
        .iter()
        .filter(|l| l.kind == DiffLineKind::FileHeader)
        .collect();
    assert_eq!(headers.len(), 3);

    assert_eq!(headers[0].content, "src/new.rs");
    assert_eq!(headers[0].file_op, Some(FileOperation::Added));

    assert_eq!(headers[1].content, "src/main.rs");
    assert_eq!(headers[1].file_op, Some(FileOperation::Modified));

    assert_eq!(headers[2].content, "old.txt");
    assert_eq!(headers[2].file_op, Some(FileOperation::Deleted));
}

#[test]
fn test_parse_diff_body_conflict_file_op() {
    let output = "\
Created conflict in test.txt:
   1     : original
        1: <<<<<<< conflict
";
    let content = Parser::parse_diff_body(output);

    let header = content
        .lines
        .iter()
        .find(|l| l.kind == DiffLineKind::FileHeader)
        .unwrap();
    assert_eq!(header.content, "test.txt");
    assert_eq!(header.file_op, Some(FileOperation::Modified));
}

#[test]
fn test_parse_git_diff_file_op_is_none() {
    // Git format intentionally does NOT set file_op (no operation info in diff --git header)
    let output = "\
diff --git a/src/main.rs b/src/main.rs
@@ -1,1 +1,1 @@
-old
+new";
    let content = Parser::parse_diff_body_git(output);

    let header = content
        .lines
        .iter()
        .find(|l| l.kind == DiffLineKind::FileHeader)
        .unwrap();
    assert_eq!(header.content, "src/main.rs");
    assert_eq!(header.file_op, None); // Must be None — git format lacks operation info
}

#[test]
fn test_parse_diff_line_file_op_is_none() {
    // Individual diff lines should never have file_op set
    let line =
        Parser::parse_diff_line("   10   10:     fn main() {", FileOperation::Modified).unwrap();
    assert_eq!(line.file_op, None);

    let line =
        Parser::parse_diff_line("        11: +       new line", FileOperation::Modified).unwrap();
    assert_eq!(line.file_op, None);
}
