use super::*;
use crate::cli::render;
use crate::core::time::now_ms;
use crate::test_support::{
    insert_project as insert_test_project, setup_test_db as setup_note_test_db, test_config,
};

#[test]
fn truncate_leaves_short_strings_alone() {
    assert_eq!(render::truncate("short", 10), "short");
}

#[test]
fn truncate_adds_ellipsis_to_long_strings() {
    assert_eq!(
        render::truncate("this is a very long title", 15),
        "this is a ve..."
    );
}

#[test]
fn truncate_handles_exact_length() {
    assert_eq!(render::truncate("exactly10!", 10), "exactly10!");
}

#[test]
fn relative_time_formats_correctly() {
    let now = now_ms();

    assert_eq!(render::format_relative_time(now - 30_000), "just now");
    assert_eq!(render::format_relative_time(now - 120_000), "2 min ago");
    assert_eq!(render::format_relative_time(now - 7_200_000), "2 hours ago");
    assert_eq!(
        render::format_relative_time(now - 172_800_000),
        "2 days ago"
    );
}

#[test]
fn format_labels_empty() {
    assert_eq!(render::format_labels(&[], 2), "");
}

#[test]
fn format_labels_single() {
    assert_eq!(render::format_labels(&["bug".to_string()], 2), "[bug]");
}

#[test]
fn format_labels_multiple() {
    let labels = vec!["bug".to_string(), "urgent".to_string()];
    assert_eq!(render::format_labels(&labels, 2), "[bug, urgent]");
}

#[test]
fn format_labels_overflow() {
    let labels = vec![
        "bug".to_string(),
        "urgent".to_string(),
        "wip".to_string(),
        "blocked".to_string(),
    ];
    assert_eq!(render::format_labels(&labels, 2), "[bug, urgent +2]");
}

#[test]
fn format_discussions_empty() {
    assert_eq!(format_discussions(0, 0).text, "");
}

#[test]
fn format_discussions_no_unresolved() {
    assert_eq!(format_discussions(5, 0).text, "5");
}

#[test]
fn format_discussions_with_unresolved() {
    let cell = format_discussions(5, 2);
    // Text contains styled ANSI for warning-colored unresolved count
    assert!(cell.text.starts_with("5/"), "got: {}", cell.text);
    assert!(cell.text.contains("2!"), "got: {}", cell.text);
}

// -----------------------------------------------------------------------
// Note query layer tests
// -----------------------------------------------------------------------

fn default_note_filters() -> NoteListFilters {
    NoteListFilters {
        limit: 50,
        project: None,
        author: None,
        note_type: None,
        include_system: false,
        for_issue_iid: None,
        for_mr_iid: None,
        note_id: None,
        gitlab_note_id: None,
        discussion_id: None,
        since: None,
        until: None,
        path: None,
        contains: None,
        resolution: None,
        sort: "created".to_string(),
        order: "desc".to_string(),
    }
}

fn insert_test_issue(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &str) {
    conn.execute(
        "INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, author_username,
         created_at, updated_at, last_seen_at)
         VALUES (?1, ?2, ?3, ?4, ?5, 'opened', 'author', 1000, 2000, 3000)",
        rusqlite::params![id, id * 10, project_id, iid, title],
    )
    .unwrap();
}

fn insert_test_mr(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &str) {
    conn.execute(
        "INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state,
         author_username, source_branch, target_branch,
         created_at, updated_at, last_seen_at)
         VALUES (?1, ?2, ?3, ?4, ?5, 'opened', 'author', 'feat', 'main',
                 1000, 2000, 3000)",
        rusqlite::params![id, id * 10, project_id, iid, title],
    )
    .unwrap();
}

#[allow(clippy::too_many_arguments)]
fn insert_test_discussion(
    conn: &Connection,
    id: i64,
    gitlab_disc_id: &str,
    project_id: i64,
    issue_id: Option<i64>,
    mr_id: Option<i64>,
    noteable_type: &str,
) {
    conn.execute(
        "INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id,
         merge_request_id, noteable_type, last_seen_at)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1000)",
        rusqlite::params![
            id,
            gitlab_disc_id,
            project_id,
            issue_id,
            mr_id,
            noteable_type
        ],
    )
    .unwrap();
}

#[allow(clippy::too_many_arguments)]
fn insert_test_note(
    conn: &Connection,
    id: i64,
    gitlab_id: i64,
    discussion_id: i64,
    project_id: i64,
    author: &str,
    body: &str,
    is_system: bool,
    created_at: i64,
    updated_at: i64,
) {
    conn.execute(
        "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
         body, is_system, created_at, updated_at, last_seen_at)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
        rusqlite::params![
            id,
            gitlab_id,
            discussion_id,
            project_id,
            author,
            body,
            is_system as i64,
            created_at,
            updated_at,
            updated_at,
        ],
    )
    .unwrap();
}

#[allow(clippy::too_many_arguments)]
fn insert_note_with_position(
    conn: &Connection,
    id: i64,
    gitlab_id: i64,
    discussion_id: i64,
    project_id: i64,
    author: &str,
    body: &str,
    created_at: i64,
    note_type: Option<&str>,
    new_path: Option<&str>,
    new_line: Option<i64>,
) {
    conn.execute(
        "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
         body, is_system, created_at, updated_at, last_seen_at,
         note_type, position_new_path, position_new_line)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
        rusqlite::params![
            id,
            gitlab_id,
            discussion_id,
            project_id,
            author,
            body,
            created_at,
            note_type,
            new_path,
            new_line,
        ],
    )
    .unwrap();
}

#[allow(clippy::too_many_arguments)]
fn insert_resolvable_note(
    conn: &Connection,
    id: i64,
    gitlab_id: i64,
    discussion_id: i64,
    project_id: i64,
    author: &str,
    body: &str,
    created_at: i64,
    resolvable: bool,
    resolved: bool,
    resolved_by: Option<&str>,
) {
    conn.execute(
        "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
         body, is_system, created_at, updated_at, last_seen_at,
         resolvable, resolved, resolved_by)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
        rusqlite::params![
            id,
            gitlab_id,
            discussion_id,
            project_id,
            author,
            body,
            created_at,
            resolvable as i64,
            resolved as i64,
            resolved_by,
        ],
    )
    .unwrap();
}

#[test]
fn test_query_notes_empty_db() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    let filters = default_note_filters();

    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 0);
    assert!(result.notes.is_empty());
}

#[test]
fn test_query_notes_basic() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 42, "Test Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Hello world",
        false,
        1000,
        2000,
    );

    let filters = default_note_filters();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes.len(), 1);
    assert_eq!(result.notes[0].author_username, "alice");
    assert_eq!(result.notes[0].body.as_deref(), Some("Hello world"));
    assert_eq!(result.notes[0].parent_iid, Some(42));
    assert_eq!(result.notes[0].parent_title.as_deref(), Some("Test Issue"));
    assert_eq!(result.notes[0].project_path, "group/project");
}

#[test]
fn test_query_notes_excludes_system_by_default() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);

    let filters = default_note_filters();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_include_system() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);

    let mut filters = default_note_filters();
    filters.include_system = true;
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 2);
}

#[test]
fn test_query_notes_filter_author() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note 1", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "Note 2", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.author = Some("alice".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_author_case_insensitive() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "Alice", "Note", false, 1000, 1000);

    let mut filters = default_note_filters();
    filters.author = Some("ALICE".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
}

#[test]
fn test_query_notes_filter_author_strips_at() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note", false, 1000, 1000);

    let mut filters = default_note_filters();
    filters.author = Some("@alice".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
}

#[test]
fn test_query_notes_filter_since() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
    let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);

    let mut filters = default_note_filters();
    filters.since = Some("2024-03-01".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "bob");
}

#[test]
fn test_query_notes_filter_until() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
    let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);

    let mut filters = default_note_filters();
    filters.until = Some("2024-03-01".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_since_until_combined() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    let ts1 = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old", false, ts1, ts1);
    let ts2 = crate::core::time::iso_to_ms("2024-03-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "Middle", false, ts2, ts2);
    let ts3 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
    insert_test_note(&conn, 3, 102, 1, 1, "carol", "New", false, ts3, ts3);

    let mut filters = default_note_filters();
    filters.since = Some("2024-02-01".to_string());
    filters.until = Some("2024-04-01".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "bob");
}

#[test]
fn test_query_notes_invalid_time_window_rejected() {
    let conn = setup_note_test_db();
    let config = test_config(None);

    let mut filters = default_note_filters();
    filters.since = Some("2024-06-01".to_string());
    filters.until = Some("2024-01-01".to_string());
    let result = query_notes(&conn, &filters, &config);
    assert!(result.is_err());
    let msg = result.unwrap_err().to_string();
    assert!(
        msg.contains("--since is after --until"),
        "Expected time window error, got: {msg}"
    );
}

#[test]
fn test_query_notes_until_date_uses_end_of_day() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    let ts = crate::core::time::iso_to_ms("2024-03-01T23:30:00Z").unwrap();
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "Late night", false, ts, ts);

    let mut filters = default_note_filters();
    filters.until = Some("2024-03-01".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(
        result.total_count, 1,
        "Note at 23:30 should be included when --until is the same date"
    );
}

#[test]
fn test_query_notes_filter_contains() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "This has a BUG in it",
        false,
        1000,
        1000,
    );
    insert_test_note(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "Everything is fine",
        false,
        2000,
        2000,
    );

    let mut filters = default_note_filters();
    filters.contains = Some("bug".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_contains_escapes_like_wildcards() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "100% done", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "100 things", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.contains = Some("100%".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_path_exact() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_note_with_position(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Change here",
        1000,
        Some("DiffNote"),
        Some("src/main.rs"),
        Some(42),
    );
    insert_note_with_position(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "And here",
        2000,
        Some("DiffNote"),
        Some("src/lib.rs"),
        Some(10),
    );

    let mut filters = default_note_filters();
    filters.path = Some("src/main.rs".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_path_prefix() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_note_with_position(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "In src",
        1000,
        Some("DiffNote"),
        Some("src/main.rs"),
        Some(42),
    );
    insert_note_with_position(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "Also in src",
        2000,
        Some("DiffNote"),
        Some("src/lib.rs"),
        Some(10),
    );
    insert_note_with_position(
        &conn,
        3,
        102,
        1,
        1,
        "carol",
        "In tests",
        3000,
        Some("DiffNote"),
        Some("tests/test.rs"),
        Some(1),
    );

    let mut filters = default_note_filters();
    filters.path = Some("src/".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 2);
}

#[test]
fn test_query_notes_filter_for_issue_requires_project() {
    let conn = setup_note_test_db();
    let config = test_config(None);

    let mut filters = default_note_filters();
    filters.for_issue_iid = Some(42);
    let result = query_notes(&conn, &filters, &config);
    assert!(result.is_err());
    let msg = result.unwrap_err().to_string();
    assert!(
        msg.contains("project context"),
        "Expected project context error, got: {msg}"
    );
}

#[test]
fn test_query_notes_filter_for_mr_requires_project() {
    let conn = setup_note_test_db();
    let config = test_config(None);

    let mut filters = default_note_filters();
    filters.for_mr_iid = Some(10);
    let result = query_notes(&conn, &filters, &config);
    assert!(result.is_err());
    let msg = result.unwrap_err().to_string();
    assert!(
        msg.contains("project context"),
        "Expected project context error, got: {msg}"
    );
}

#[test]
fn test_query_notes_filter_for_issue_with_project() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 42, "Target Issue");
    insert_test_issue(&conn, 2, 1, 43, "Other Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_discussion(&conn, 2, "disc-2", 1, Some(2), None, "Issue");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "On issue 42",
        false,
        1000,
        1000,
    );
    insert_test_note(&conn, 2, 101, 2, 1, "bob", "On issue 43", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.for_issue_iid = Some(42);
    filters.project = Some("group/project".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_for_issue_uses_default_project() {
    let conn = setup_note_test_db();
    let config = test_config(Some("group/project"));
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 42, "Target Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "On issue 42",
        false,
        1000,
        1000,
    );

    let mut filters = default_note_filters();
    filters.for_issue_iid = Some(42);
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
}

#[test]
fn test_query_notes_filter_for_mr_with_project() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_mr(&conn, 1, 1, 10, "Target MR");
    insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "On MR 10", false, 1000, 1000);

    let mut filters = default_note_filters();
    filters.for_mr_iid = Some(10);
    filters.project = Some("group/project".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_resolution_unresolved() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_resolvable_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Unresolved",
        1000,
        true,
        false,
        None,
    );
    insert_resolvable_note(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "Resolved",
        2000,
        true,
        true,
        Some("carol"),
    );
    insert_test_note(
        &conn,
        3,
        102,
        1,
        1,
        "dave",
        "Not resolvable",
        false,
        3000,
        3000,
    );

    let mut filters = default_note_filters();
    filters.resolution = Some("unresolved".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_resolution_resolved() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_resolvable_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Unresolved",
        1000,
        true,
        false,
        None,
    );
    insert_resolvable_note(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "Resolved",
        2000,
        true,
        true,
        Some("carol"),
    );

    let mut filters = default_note_filters();
    filters.resolution = Some("resolved".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "bob");
    assert!(result.notes[0].resolved);
    assert_eq!(result.notes[0].resolved_by.as_deref(), Some("carol"));
}

#[test]
fn test_query_notes_sort_created_desc() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
    insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);

    let filters = default_note_filters();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.notes[0].author_username, "carol");
    assert_eq!(result.notes[1].author_username, "bob");
    assert_eq!(result.notes[2].author_username, "alice");
}

#[test]
fn test_query_notes_sort_created_asc() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
    insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);

    let mut filters = default_note_filters();
    filters.order = "asc".to_string();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.notes[0].author_username, "alice");
    assert_eq!(result.notes[1].author_username, "bob");
    assert_eq!(result.notes[2].author_username, "carol");
}

#[test]
fn test_query_notes_deterministic_tiebreak() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 1000, 1000);
    insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 1000, 1000);

    let filters = default_note_filters();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.notes[0].id, 3);
    assert_eq!(result.notes[1].id, 2);
    assert_eq!(result.notes[2].id, 1);

    let mut filters = default_note_filters();
    filters.order = "asc".to_string();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.notes[0].id, 1);
    assert_eq!(result.notes[1].id, 2);
    assert_eq!(result.notes[2].id, 3);
}

#[test]
fn test_query_notes_limit() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    for i in 1..=10 {
        insert_test_note(
            &conn,
            i,
            100 + i,
            1,
            1,
            "alice",
            &format!("Note {i}"),
            false,
            i * 1000,
            i * 1000,
        );
    }

    let mut filters = default_note_filters();
    filters.limit = 3;
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 10);
    assert_eq!(result.notes.len(), 3);
}

#[test]
fn test_query_notes_combined_filters() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Found a bug here",
        false,
        1000,
        1000,
    );
    insert_test_note(
        &conn,
        2,
        101,
        1,
        1,
        "alice",
        "Looks good",
        false,
        2000,
        2000,
    );
    insert_test_note(
        &conn,
        3,
        102,
        1,
        1,
        "bob",
        "Another bug fix",
        false,
        3000,
        3000,
    );

    let mut filters = default_note_filters();
    filters.author = Some("alice".to_string());
    filters.contains = Some("bug".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].id, 1);
}

#[test]
fn test_query_notes_filter_note_type() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_note_with_position(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "Diff comment",
        1000,
        Some("DiffNote"),
        Some("src/main.rs"),
        Some(10),
    );
    insert_test_note(
        &conn,
        2,
        101,
        1,
        1,
        "bob",
        "Discussion note",
        false,
        2000,
        2000,
    );

    let mut filters = default_note_filters();
    filters.note_type = Some("DiffNote".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_discussion_id() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-aaa", 1, Some(1), None, "Issue");
    insert_test_discussion(&conn, 2, "disc-bbb", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "In disc A", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 2, 1, "bob", "In disc B", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.discussion_id = Some("disc-aaa".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_filter_note_id() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.note_id = Some(2);
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "bob");
}

#[test]
fn test_query_notes_filter_gitlab_note_id() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
    insert_test_note(&conn, 2, 200, 1, 1, "bob", "Second", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.gitlab_note_id = Some(200);
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "bob");
}

#[test]
fn test_query_notes_filter_project() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project-a");
    insert_test_project(&conn, 2, "group/project-b");
    insert_test_issue(&conn, 1, 1, 1, "Issue A");
    insert_test_issue(&conn, 2, 2, 1, "Issue B");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_discussion(&conn, 2, "disc-2", 2, Some(2), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "In A", false, 1000, 1000);
    insert_test_note(&conn, 2, 101, 2, 2, "bob", "In B", false, 2000, 2000);

    let mut filters = default_note_filters();
    filters.project = Some("group/project-a".to_string());
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(result.notes[0].author_username, "alice");
}

#[test]
fn test_query_notes_mr_parent() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_mr(&conn, 1, 1, 99, "My MR");
    insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
    insert_test_note(
        &conn,
        1,
        100,
        1,
        1,
        "alice",
        "MR comment",
        false,
        1000,
        1000,
    );

    let filters = default_note_filters();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.total_count, 1);
    assert_eq!(
        result.notes[0].noteable_type.as_deref(),
        Some("MergeRequest")
    );
    assert_eq!(result.notes[0].parent_iid, Some(99));
    assert_eq!(result.notes[0].parent_title.as_deref(), Some("My MR"));
}

#[test]
fn test_note_list_row_json_conversion() {
    let row = NoteListRow {
        id: 1,
        gitlab_id: 100,
        author_username: "alice".to_string(),
        body: Some("Test body".to_string()),
        note_type: Some("DiffNote".to_string()),
        is_system: false,
        created_at: 1_705_315_800_000,
        updated_at: 1_705_315_800_000,
        position_new_path: Some("src/main.rs".to_string()),
        position_new_line: Some(42),
        position_old_path: None,
        position_old_line: None,
        resolvable: true,
        resolved: false,
        resolved_by: None,
        noteable_type: Some("Issue".to_string()),
        parent_iid: Some(5),
        parent_title: Some("Test Issue".to_string()),
        project_path: "group/project".to_string(),
    };

    let json_row = NoteListRowJson::from(&row);
    assert_eq!(json_row.id, 1);
    assert_eq!(json_row.gitlab_id, 100);
    assert_eq!(json_row.author_username, "alice");
    assert!(json_row.created_at_iso.contains("2024-01-15"));
    assert!(json_row.updated_at_iso.contains("2024-01-15"));
    assert_eq!(json_row.position_new_path.as_deref(), Some("src/main.rs"));
    assert_eq!(json_row.position_new_line, Some(42));
    assert!(!json_row.is_system);
    assert!(json_row.resolvable);
    assert!(!json_row.resolved);
}

#[test]
fn test_note_list_result_json_conversion() {
    let result = NoteListResult {
        notes: vec![NoteListRow {
            id: 1,
            gitlab_id: 100,
            author_username: "alice".to_string(),
            body: Some("Test".to_string()),
            note_type: None,
            is_system: false,
            created_at: 1000,
            updated_at: 2000,
            position_new_path: None,
            position_new_line: None,
            position_old_path: None,
            position_old_line: None,
            resolvable: false,
            resolved: false,
            resolved_by: None,
            noteable_type: Some("Issue".to_string()),
            parent_iid: Some(1),
            parent_title: Some("Issue".to_string()),
            project_path: "group/project".to_string(),
        }],
        total_count: 5,
    };

    let json_result = NoteListResultJson::from(&result);
    assert_eq!(json_result.total_count, 5);
    assert_eq!(json_result.showing, 1);
    assert_eq!(json_result.notes.len(), 1);
}

#[test]
fn test_query_notes_sort_updated() {
    let conn = setup_note_test_db();
    let config = test_config(None);
    insert_test_project(&conn, 1, "group/project");
    insert_test_issue(&conn, 1, 1, 1, "Issue");
    insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
    insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 3000);
    insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 2000, 1000);
    insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 3000, 2000);

    let mut filters = default_note_filters();
    filters.sort = "updated".to_string();
    filters.order = "desc".to_string();
    let result = query_notes(&conn, &filters, &config).unwrap();
    assert_eq!(result.notes[0].author_username, "alice");
    assert_eq!(result.notes[1].author_username, "carol");
    assert_eq!(result.notes[2].author_username, "bob");
}

#[test]
fn test_note_escape_like() {
    assert_eq!(note_escape_like("normal/path"), "normal/path");
    assert_eq!(note_escape_like("has_underscore"), "has\\_underscore");
    assert_eq!(note_escape_like("has%percent"), "has\\%percent");
    assert_eq!(note_escape_like("has\\backslash"), "has\\\\backslash");
}

// -----------------------------------------------------------------------
// Note output formatting tests
// -----------------------------------------------------------------------

#[test]
fn test_truncate_note_body() {
    let short = "short body";
    assert_eq!(truncate_body(short, 60), "short body");

    let long: String = "a".repeat(200);
    let result = truncate_body(&long, 60);
    assert_eq!(result.chars().count(), 63); // 60 chars + "..."
    assert!(result.ends_with("..."));
}

#[test]
fn test_jsonl_output_one_per_line() {
    let result = NoteListResult {
        notes: vec![
            NoteListRow {
                id: 1,
                gitlab_id: 100,
                author_username: "alice".to_string(),
                body: Some("First note".to_string()),
                note_type: None,
                is_system: false,
                created_at: 1_000_000,
                updated_at: 2_000_000,
                position_new_path: None,
                position_new_line: None,
                position_old_path: None,
                position_old_line: None,
                resolvable: false,
                resolved: false,
                resolved_by: None,
                noteable_type: None,
                parent_iid: None,
                parent_title: None,
                project_path: "group/project".to_string(),
            },
            NoteListRow {
                id: 2,
                gitlab_id: 101,
                author_username: "bob".to_string(),
                body: Some("Second note".to_string()),
                note_type: Some("DiffNote".to_string()),
                is_system: false,
                created_at: 3_000_000,
                updated_at: 4_000_000,
                position_new_path: None,
                position_new_line: None,
                position_old_path: None,
                position_old_line: None,
                resolvable: false,
                resolved: false,
                resolved_by: None,
                noteable_type: None,
                parent_iid: None,
                parent_title: None,
                project_path: "group/project".to_string(),
            },
        ],
        total_count: 2,
    };

    // Each note should produce valid JSON when serialized individually
    for note in &result.notes {
        let json_row = NoteListRowJson::from(note);
        let json_str = serde_json::to_string(&json_row).unwrap();
        // Verify it parses back as valid JSON
        let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
        // Verify no embedded newlines in the JSON line
        assert!(!json_str.contains('\n'));
    }
}

#[test]
fn test_format_note_parent_variants() {
    assert_eq!(format_note_parent(Some("Issue"), Some(42)), "Issue #42");
    assert_eq!(format_note_parent(Some("MergeRequest"), Some(99)), "MR !99");
    assert_eq!(format_note_parent(None, None), "-");
    assert_eq!(format_note_parent(Some("Issue"), None), "-");
}

#[test]
fn test_format_note_type_variants() {
    assert_eq!(format_note_type(Some("DiffNote")), "Diff");
    assert_eq!(format_note_type(Some("DiscussionNote")), "Disc");
    assert_eq!(format_note_type(None), "-");
    assert_eq!(format_note_type(Some("Other")), "-");
}
