use super::*;

// --- normalize_title_for_embedding tests ---

#[test]
fn test_normalize_title_strips_draft_resolve_quotes() {
    assert_eq!(
        normalize_title_for_embedding("Draft: Resolve \"Analytics Studio: Subformulas\""),
        "Analytics Studio: Subformulas"
    );
}

#[test]
fn test_normalize_title_strips_resolve_quotes() {
    assert_eq!(
        normalize_title_for_embedding("Resolve \"RUL Report: Use param_trends from S3\""),
        "RUL Report: Use param_trends from S3"
    );
}

#[test]
fn test_normalize_title_strips_draft_prefix() {
    assert_eq!(
        normalize_title_for_embedding("Draft: Implement JWT authentication"),
        "Implement JWT authentication"
    );
}

#[test]
fn test_normalize_title_strips_wip_prefix() {
    assert_eq!(
        normalize_title_for_embedding("WIP: Implement JWT authentication"),
        "Implement JWT authentication"
    );
}

#[test]
fn test_normalize_title_strips_draft_wip_combined() {
    assert_eq!(
        normalize_title_for_embedding("Draft: WIP: Fix auth"),
        "Fix auth"
    );
}

#[test]
fn test_normalize_title_no_change_for_normal_title() {
    assert_eq!(
        normalize_title_for_embedding("Implement JWT authentication"),
        "Implement JWT authentication"
    );
}

#[test]
fn test_normalize_title_case_insensitive_draft() {
    assert_eq!(
        normalize_title_for_embedding("draft: Resolve \"Some Issue\""),
        "Some Issue"
    );
}

#[test]
fn test_normalize_title_case_insensitive_wip() {
    assert_eq!(normalize_title_for_embedding("wip: Something"), "Something");
}

#[test]
fn test_normalize_title_untitled_passthrough() {
    assert_eq!(normalize_title_for_embedding("(untitled)"), "(untitled)");
}

#[test]
fn test_normalize_title_resolve_without_quotes_unchanged() {
    // "Resolve something" without quotes is not the GitLab pattern
    assert_eq!(
        normalize_title_for_embedding("Resolve the flaky test"),
        "Resolve the flaky test"
    );
}

#[test]
fn test_normalize_title_empty_after_strip_returns_original() {
    // Edge case: "Draft: " with nothing after → return original
    assert_eq!(normalize_title_for_embedding("Draft: "), "Draft: ");
}

#[test]
fn test_normalize_title_resolve_empty_quotes() {
    // Edge case: Resolve "" → return original (empty inner text)
    assert_eq!(
        normalize_title_for_embedding("Resolve \"\""),
        "Resolve \"\""
    );
}

#[test]
fn test_normalize_title_non_ascii_does_not_panic() {
    // Emoji at start: byte offsets 4 and 8 fall mid-character.
    // Must not panic — should return the title unchanged.
    assert_eq!(
        normalize_title_for_embedding("\u{1F389}\u{1F389} celebration"),
        "\u{1F389}\u{1F389} celebration"
    );
    // Accented characters
    assert_eq!(
        normalize_title_for_embedding("\u{00DC}berpr\u{00FC}fung der Daten"),
        "\u{00DC}berpr\u{00FC}fung der Daten"
    );
}

// --- MR document uses normalized title in content_text ---

#[test]
fn test_mr_document_normalizes_draft_resolve_title() {
    let conn = setup_mr_test_db();
    insert_mr(
        &conn,
        1,
        4064,
        Some("Draft: Resolve \"Analytics Studio: Subformulas\""),
        Some("Implements subformula support"),
        Some("opened"),
        Some("dev"),
        Some("feature/subformulas"),
        Some("main"),
        None,
    );

    let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
    // content_text should use the normalized title (no boilerplate)
    assert!(
        doc.content_text
            .starts_with("[[MergeRequest]] !4064: Analytics Studio: Subformulas\n")
    );
    // but DocumentData.title preserves the original for display
    assert_eq!(
        doc.title,
        Some("Draft: Resolve \"Analytics Studio: Subformulas\"".to_string())
    );
}

// --- Issue document uses normalized title in content_text ---

#[test]
fn test_issue_document_normalizes_draft_title() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        100,
        Some("Draft: WIP: Rethink caching strategy"),
        Some("We should reconsider..."),
        "opened",
        Some("alice"),
        None,
    );

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert!(
        doc.content_text
            .starts_with("[[Issue]] #100: Rethink caching strategy\n")
    );
    // Original title preserved for display
    assert_eq!(
        doc.title,
        Some("Draft: WIP: Rethink caching strategy".to_string())
    );
}

#[test]
fn test_source_type_parse_aliases() {
    assert_eq!(SourceType::parse("issue"), Some(SourceType::Issue));
    assert_eq!(SourceType::parse("issues"), Some(SourceType::Issue));
    assert_eq!(SourceType::parse("mr"), Some(SourceType::MergeRequest));
    assert_eq!(SourceType::parse("mrs"), Some(SourceType::MergeRequest));
    assert_eq!(
        SourceType::parse("merge_request"),
        Some(SourceType::MergeRequest)
    );
    assert_eq!(
        SourceType::parse("merge_requests"),
        Some(SourceType::MergeRequest)
    );
    assert_eq!(
        SourceType::parse("discussion"),
        Some(SourceType::Discussion)
    );
    assert_eq!(
        SourceType::parse("discussions"),
        Some(SourceType::Discussion)
    );
    assert_eq!(SourceType::parse("invalid"), None);
    assert_eq!(SourceType::parse("ISSUE"), Some(SourceType::Issue));
}

#[test]
fn test_source_type_parse_note() {
    assert_eq!(SourceType::parse("note"), Some(SourceType::Note));
}

#[test]
fn test_source_type_note_as_str() {
    assert_eq!(SourceType::Note.as_str(), "note");
}

#[test]
fn test_source_type_note_display() {
    assert_eq!(format!("{}", SourceType::Note), "note");
}

#[test]
fn test_source_type_parse_notes_alias() {
    assert_eq!(SourceType::parse("notes"), Some(SourceType::Note));
}

#[test]
fn test_source_type_as_str() {
    assert_eq!(SourceType::Issue.as_str(), "issue");
    assert_eq!(SourceType::MergeRequest.as_str(), "merge_request");
    assert_eq!(SourceType::Discussion.as_str(), "discussion");
}

#[test]
fn test_source_type_display() {
    assert_eq!(format!("{}", SourceType::Issue), "issue");
    assert_eq!(format!("{}", SourceType::MergeRequest), "merge_request");
    assert_eq!(format!("{}", SourceType::Discussion), "discussion");
}

#[test]
fn test_content_hash_deterministic() {
    let hash1 = compute_content_hash("hello");
    let hash2 = compute_content_hash("hello");
    assert_eq!(hash1, hash2);
    assert!(!hash1.is_empty());
    assert_eq!(hash1.len(), 64);
}

#[test]
fn test_content_hash_different_inputs() {
    let hash1 = compute_content_hash("hello");
    let hash2 = compute_content_hash("world");
    assert_ne!(hash1, hash2);
}

#[test]
fn test_content_hash_empty() {
    let hash = compute_content_hash("");
    assert_eq!(hash.len(), 64);
}

#[test]
fn test_list_hash_order_independent() {
    let hash1 = compute_list_hash(&["b".to_string(), "a".to_string()]);
    let hash2 = compute_list_hash(&["a".to_string(), "b".to_string()]);
    assert_eq!(hash1, hash2);
}

#[test]
fn test_list_hash_empty() {
    let hash = compute_list_hash(&[]);
    assert_eq!(hash.len(), 64);
    let hash2 = compute_list_hash(&[]);
    assert_eq!(hash, hash2);
}

fn setup_test_db() -> Connection {
    let conn = Connection::open_in_memory().unwrap();
    conn.execute_batch(
        "
        CREATE TABLE projects (
            id INTEGER PRIMARY KEY,
            gitlab_project_id INTEGER UNIQUE NOT NULL,
            path_with_namespace TEXT NOT NULL,
            default_branch TEXT,
            web_url TEXT,
            created_at INTEGER,
            updated_at INTEGER,
            raw_payload_id INTEGER
        );
        CREATE TABLE issues (
            id INTEGER PRIMARY KEY,
            gitlab_id INTEGER UNIQUE NOT NULL,
            project_id INTEGER NOT NULL REFERENCES projects(id),
            iid INTEGER NOT NULL,
            title TEXT,
            description TEXT,
            state TEXT NOT NULL,
            author_username TEXT,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            last_seen_at INTEGER NOT NULL,
            discussions_synced_for_updated_at INTEGER,
            resource_events_synced_for_updated_at INTEGER,
            web_url TEXT,
            raw_payload_id INTEGER
        );
        CREATE TABLE labels (
            id INTEGER PRIMARY KEY,
            gitlab_id INTEGER,
            project_id INTEGER NOT NULL REFERENCES projects(id),
            name TEXT NOT NULL,
            color TEXT,
            description TEXT
        );
        CREATE TABLE issue_labels (
            issue_id INTEGER NOT NULL REFERENCES issues(id),
            label_id INTEGER NOT NULL REFERENCES labels(id),
            PRIMARY KEY(issue_id, label_id)
        );
    ",
    )
    .unwrap();

    conn.execute(
        "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url) VALUES (1, 100, 'group/project-one', 'https://gitlab.example.com/group/project-one')",
        [],
    ).unwrap();

    conn
}

#[allow(clippy::too_many_arguments)]
fn insert_issue(
    conn: &Connection,
    id: i64,
    iid: i64,
    title: Option<&str>,
    description: Option<&str>,
    state: &str,
    author: Option<&str>,
    web_url: Option<&str>,
) {
    conn.execute(
        "INSERT INTO issues (id, gitlab_id, project_id, iid, title, description, state, author_username, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, 1000, 2000, 3000, ?8)",
        rusqlite::params![id, id * 10, iid, title, description, state, author, web_url],
    ).unwrap();
}

fn insert_label(conn: &Connection, id: i64, name: &str) {
    conn.execute(
        "INSERT INTO labels (id, project_id, name) VALUES (?1, 1, ?2)",
        rusqlite::params![id, name],
    )
    .unwrap();
}

fn link_issue_label(conn: &Connection, issue_id: i64, label_id: i64) {
    conn.execute(
        "INSERT INTO issue_labels (issue_id, label_id) VALUES (?1, ?2)",
        rusqlite::params![issue_id, label_id],
    )
    .unwrap();
}

#[test]
fn test_issue_document_format() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        234,
        Some("Authentication redesign"),
        Some("We need to modernize our authentication system..."),
        "opened",
        Some("johndoe"),
        Some("https://gitlab.example.com/group/project-one/-/issues/234"),
    );
    insert_label(&conn, 1, "auth");
    insert_label(&conn, 2, "bug");
    link_issue_label(&conn, 1, 1);
    link_issue_label(&conn, 1, 2);

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.source_type, SourceType::Issue);
    assert_eq!(doc.source_id, 1);
    assert_eq!(doc.project_id, 1);
    assert_eq!(doc.author_username, Some("johndoe".to_string()));
    assert!(
        doc.content_text
            .starts_with("[[Issue]] #234: Authentication redesign\n")
    );
    assert!(doc.content_text.contains("Project: group/project-one\n"));
    assert!(
        doc.content_text
            .contains("URL: https://gitlab.example.com/group/project-one/-/issues/234\n")
    );
    assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
    assert!(doc.content_text.contains("State: opened\n"));
    assert!(doc.content_text.contains("Author: @johndoe\n"));
    assert!(
        doc.content_text
            .contains("--- Description ---\n\nWe need to modernize our authentication system...")
    );
    assert!(!doc.is_truncated);
    assert!(doc.paths.is_empty());
}

#[test]
fn test_issue_not_found() {
    let conn = setup_test_db();
    let result = extract_issue_document(&conn, 999).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_issue_no_description() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Quick fix"),
        None,
        "opened",
        Some("alice"),
        None,
    );

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert!(!doc.content_text.contains("--- Description ---"));
    assert!(doc.content_text.contains("[[Issue]] #10: Quick fix\n"));
}

#[test]
fn test_issue_labels_sorted() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("Body"),
        "opened",
        Some("bob"),
        None,
    );
    insert_label(&conn, 1, "zeta");
    insert_label(&conn, 2, "alpha");
    insert_label(&conn, 3, "middle");
    link_issue_label(&conn, 1, 1);
    link_issue_label(&conn, 1, 2);
    link_issue_label(&conn, 1, 3);

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.labels, vec!["alpha", "middle", "zeta"]);
    assert!(
        doc.content_text
            .contains("Labels: [\"alpha\",\"middle\",\"zeta\"]")
    );
}

#[test]
fn test_issue_no_labels() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("Body"),
        "opened",
        None,
        None,
    );

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert!(doc.labels.is_empty());
    assert!(doc.content_text.contains("Labels: []\n"));
}

#[test]
fn test_issue_hash_deterministic() {
    let conn = setup_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("Body"),
        "opened",
        Some("alice"),
        None,
    );

    let doc1 = extract_issue_document(&conn, 1).unwrap().unwrap();
    let doc2 = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc1.content_hash, doc2.content_hash);
    assert_eq!(doc1.labels_hash, doc2.labels_hash);
    assert_eq!(doc1.content_hash.len(), 64);
}

#[test]
fn test_issue_empty_description() {
    let conn = setup_test_db();
    insert_issue(&conn, 1, 10, Some("Test"), Some(""), "opened", None, None);

    let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("--- Description ---\n\n"));
}

fn setup_mr_test_db() -> Connection {
    let conn = setup_test_db();
    conn.execute_batch(
        "
        CREATE TABLE merge_requests (
            id INTEGER PRIMARY KEY,
            gitlab_id INTEGER UNIQUE NOT NULL,
            project_id INTEGER NOT NULL REFERENCES projects(id),
            iid INTEGER NOT NULL,
            title TEXT,
            description TEXT,
            state TEXT,
            draft INTEGER NOT NULL DEFAULT 0,
            author_username TEXT,
            source_branch TEXT,
            target_branch TEXT,
            head_sha TEXT,
            references_short TEXT,
            references_full TEXT,
            detailed_merge_status TEXT,
            merge_user_username TEXT,
            created_at INTEGER,
            updated_at INTEGER,
            merged_at INTEGER,
            closed_at INTEGER,
            last_seen_at INTEGER NOT NULL,
            discussions_synced_for_updated_at INTEGER,
            discussions_sync_last_attempt_at INTEGER,
            discussions_sync_attempts INTEGER DEFAULT 0,
            discussions_sync_last_error TEXT,
            resource_events_synced_for_updated_at INTEGER,
            web_url TEXT,
            raw_payload_id INTEGER
        );
        CREATE TABLE mr_labels (
            merge_request_id INTEGER REFERENCES merge_requests(id),
            label_id INTEGER REFERENCES labels(id),
            PRIMARY KEY(merge_request_id, label_id)
        );
    ",
    )
    .unwrap();
    conn
}

#[allow(clippy::too_many_arguments)]
fn insert_mr(
    conn: &Connection,
    id: i64,
    iid: i64,
    title: Option<&str>,
    description: Option<&str>,
    state: Option<&str>,
    author: Option<&str>,
    source_branch: Option<&str>,
    target_branch: Option<&str>,
    web_url: Option<&str>,
) {
    conn.execute(
        "INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, description, state, author_username, source_branch, target_branch, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1000, 2000, 3000, ?10)",
        rusqlite::params![id, id * 10, iid, title, description, state, author, source_branch, target_branch, web_url],
    ).unwrap();
}

fn link_mr_label(conn: &Connection, mr_id: i64, label_id: i64) {
    conn.execute(
        "INSERT INTO mr_labels (merge_request_id, label_id) VALUES (?1, ?2)",
        rusqlite::params![mr_id, label_id],
    )
    .unwrap();
}

#[test]
fn test_mr_document_format() {
    let conn = setup_mr_test_db();
    insert_mr(
        &conn,
        1,
        456,
        Some("Implement JWT authentication"),
        Some("This MR implements JWT-based authentication..."),
        Some("opened"),
        Some("johndoe"),
        Some("feature/jwt-auth"),
        Some("main"),
        Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
    );
    insert_label(&conn, 1, "auth");
    insert_label(&conn, 2, "feature");
    link_mr_label(&conn, 1, 1);
    link_mr_label(&conn, 1, 2);

    let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.source_type, SourceType::MergeRequest);
    assert_eq!(doc.source_id, 1);
    assert!(
        doc.content_text
            .starts_with("[[MergeRequest]] !456: Implement JWT authentication\n")
    );
    assert!(doc.content_text.contains("Project: group/project-one\n"));
    assert!(
        doc.content_text
            .contains("Labels: [\"auth\",\"feature\"]\n")
    );
    assert!(doc.content_text.contains("State: opened\n"));
    assert!(doc.content_text.contains("Author: @johndoe\n"));
    assert!(
        doc.content_text
            .contains("Source: feature/jwt-auth -> main\n")
    );
    assert!(
        doc.content_text
            .contains("--- Description ---\n\nThis MR implements JWT-based authentication...")
    );
}

#[test]
fn test_mr_not_found() {
    let conn = setup_mr_test_db();
    let result = extract_mr_document(&conn, 999).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_mr_no_description() {
    let conn = setup_mr_test_db();
    insert_mr(
        &conn,
        1,
        10,
        Some("Quick fix"),
        None,
        Some("merged"),
        Some("alice"),
        Some("fix/bug"),
        Some("main"),
        None,
    );

    let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
    assert!(!doc.content_text.contains("--- Description ---"));
    assert!(
        doc.content_text
            .contains("[[MergeRequest]] !10: Quick fix\n")
    );
}

#[test]
fn test_mr_branch_info() {
    let conn = setup_mr_test_db();
    insert_mr(
        &conn,
        1,
        10,
        Some("Test"),
        Some("Body"),
        Some("opened"),
        None,
        Some("feature/foo"),
        Some("develop"),
        None,
    );

    let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
    assert!(
        doc.content_text
            .contains("Source: feature/foo -> develop\n")
    );
}

#[test]
fn test_mr_no_branches() {
    let conn = setup_mr_test_db();
    insert_mr(
        &conn,
        1,
        10,
        Some("Test"),
        None,
        Some("opened"),
        None,
        None,
        None,
        None,
    );

    let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
    assert!(!doc.content_text.contains("Source:"));
}

fn setup_discussion_test_db() -> Connection {
    let conn = setup_mr_test_db();
    conn.execute_batch(
        "
        CREATE TABLE discussions (
            id INTEGER PRIMARY KEY,
            gitlab_discussion_id TEXT NOT NULL,
            project_id INTEGER NOT NULL REFERENCES projects(id),
            issue_id INTEGER REFERENCES issues(id),
            merge_request_id INTEGER,
            noteable_type TEXT NOT NULL,
            individual_note INTEGER NOT NULL DEFAULT 0,
            first_note_at INTEGER,
            last_note_at INTEGER,
            last_seen_at INTEGER NOT NULL,
            resolvable INTEGER NOT NULL DEFAULT 0,
            resolved INTEGER NOT NULL DEFAULT 0
        );
        CREATE TABLE notes (
            id INTEGER PRIMARY KEY,
            gitlab_id INTEGER UNIQUE NOT NULL,
            discussion_id INTEGER NOT NULL REFERENCES discussions(id),
            project_id INTEGER NOT NULL REFERENCES projects(id),
            note_type TEXT,
            is_system INTEGER NOT NULL DEFAULT 0,
            author_username TEXT,
            body TEXT,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            last_seen_at INTEGER NOT NULL,
            position INTEGER,
            resolvable INTEGER NOT NULL DEFAULT 0,
            resolved INTEGER NOT NULL DEFAULT 0,
            resolved_by TEXT,
            resolved_at INTEGER,
            position_old_path TEXT,
            position_new_path TEXT,
            position_old_line INTEGER,
            position_new_line INTEGER,
            raw_payload_id INTEGER
        );
    ",
    )
    .unwrap();
    conn
}

fn insert_discussion(
    conn: &Connection,
    id: i64,
    noteable_type: &str,
    issue_id: Option<i64>,
    mr_id: Option<i64>,
) {
    conn.execute(
        "INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, merge_request_id, noteable_type, last_seen_at) VALUES (?1, ?2, 1, ?3, ?4, ?5, 3000)",
        rusqlite::params![id, format!("disc_{}", id), issue_id, mr_id, noteable_type],
    ).unwrap();
}

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

#[test]
fn test_discussion_document_format() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        234,
        Some("Authentication redesign"),
        Some("desc"),
        "opened",
        Some("johndoe"),
        Some("https://gitlab.example.com/group/project-one/-/issues/234"),
    );
    insert_label(&conn, 1, "auth");
    insert_label(&conn, 2, "bug");
    link_issue_label(&conn, 1, 1);
    link_issue_label(&conn, 1, 2);
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        12345,
        1,
        Some("johndoe"),
        Some("I think we should move to JWT-based auth..."),
        1710460800000,
        false,
        None,
        None,
    );
    insert_note(
        &conn,
        2,
        12346,
        1,
        Some("janedoe"),
        Some("Agreed. What about refresh token strategy?"),
        1710460800000,
        false,
        None,
        None,
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.source_type, SourceType::Discussion);
    assert!(
        doc.content_text
            .starts_with("[[Discussion]] Issue #234: Authentication redesign\n")
    );
    assert!(doc.content_text.contains("Project: group/project-one\n"));
    assert!(
        doc.content_text.contains(
            "URL: https://gitlab.example.com/group/project-one/-/issues/234#note_12345\n"
        )
    );
    assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
    assert!(doc.content_text.contains("--- Thread ---"));
    assert!(
        doc.content_text
            .contains("@johndoe (2024-03-15):\nI think we should move to JWT-based auth...")
    );
    assert!(
        doc.content_text
            .contains("@janedoe (2024-03-15):\nAgreed. What about refresh token strategy?")
    );
    assert_eq!(doc.author_username, Some("johndoe".to_string()));
    assert!(doc.title.is_none());
}

#[test]
fn test_discussion_not_found() {
    let conn = setup_discussion_test_db();
    let result = extract_discussion_document(&conn, 999).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_discussion_parent_deleted() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        99,
        10,
        Some("To be deleted"),
        None,
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(99), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Hello"),
        1000,
        false,
        None,
        None,
    );
    conn.execute("PRAGMA foreign_keys = OFF", []).unwrap();
    conn.execute("DELETE FROM issues WHERE id = 99", [])
        .unwrap();
    conn.execute("PRAGMA foreign_keys = ON", []).unwrap();

    let result = extract_discussion_document(&conn, 1).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_discussion_system_notes_excluded() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        Some("alice"),
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Real comment"),
        1000,
        false,
        None,
        None,
    );
    insert_note(
        &conn,
        2,
        101,
        1,
        Some("bot"),
        Some("assigned to @alice"),
        2000,
        true,
        None,
        None,
    );
    insert_note(
        &conn,
        3,
        102,
        1,
        Some("bob"),
        Some("Follow-up"),
        3000,
        false,
        None,
        None,
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("@alice"));
    assert!(doc.content_text.contains("@bob"));
    assert!(!doc.content_text.contains("assigned to"));
}

#[test]
fn test_discussion_diffnote_paths() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Comment on code"),
        1000,
        false,
        Some("src/old.rs"),
        Some("src/new.rs"),
    );
    insert_note(
        &conn,
        2,
        101,
        1,
        Some("bob"),
        Some("Reply"),
        2000,
        false,
        Some("src/old.rs"),
        Some("src/new.rs"),
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.paths, vec!["src/new.rs", "src/old.rs"]);
    assert!(
        doc.content_text
            .contains("Files: [\"src/new.rs\",\"src/old.rs\"]")
    );
}

#[test]
fn test_discussion_url_construction() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        Some("https://gitlab.example.com/group/project-one/-/issues/10"),
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        54321,
        1,
        Some("alice"),
        Some("Hello"),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert_eq!(
        doc.url,
        Some("https://gitlab.example.com/group/project-one/-/issues/10#note_54321".to_string())
    );
}

#[test]
fn test_discussion_uses_parent_labels() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_label(&conn, 1, "backend");
    insert_label(&conn, 2, "api");
    link_issue_label(&conn, 1, 1);
    link_issue_label(&conn, 1, 2);
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Comment"),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.labels, vec!["api", "backend"]);
}

#[test]
fn test_discussion_on_mr() {
    let conn = setup_discussion_test_db();
    insert_mr(
        &conn,
        1,
        456,
        Some("JWT Auth"),
        Some("desc"),
        Some("opened"),
        Some("johndoe"),
        Some("feature/jwt"),
        Some("main"),
        Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
    );
    insert_discussion(&conn, 1, "MergeRequest", None, Some(1));
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("LGTM"),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
    assert!(
        doc.content_text
            .contains("[[Discussion]] MR !456: JWT Auth\n")
    );
}

#[test]
fn test_discussion_all_system_notes() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("bot"),
        Some("assigned to @alice"),
        1000,
        true,
        None,
        None,
    );

    let result = extract_discussion_document(&conn, 1).unwrap();
    assert!(result.is_none());
}

#[allow(clippy::too_many_arguments)]
fn insert_note_with_type(
    conn: &Connection,
    id: i64,
    gitlab_id: i64,
    discussion_id: i64,
    author: Option<&str>,
    body: Option<&str>,
    created_at: i64,
    is_system: bool,
    old_path: Option<&str>,
    new_path: Option<&str>,
    old_line: Option<i64>,
    new_line: Option<i64>,
    note_type: Option<&str>,
    resolvable: bool,
    resolved: bool,
) {
    conn.execute(
        "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position_old_path, position_new_path, position_old_line, position_new_line, note_type, resolvable, resolved) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6, ?6, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
        rusqlite::params![id, gitlab_id, discussion_id, author, body, created_at, is_system as i32, old_path, new_path, old_line, new_line, note_type, resolvable as i32, resolved as i32],
    ).unwrap();
}

#[test]
fn test_note_document_basic_format() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        42,
        Some("Fix login bug"),
        Some("desc"),
        "opened",
        Some("johndoe"),
        Some("https://gitlab.example.com/group/project-one/-/issues/42"),
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        12345,
        1,
        Some("alice"),
        Some("This looks like a race condition"),
        1710460800000,
        false,
        None,
        None,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.source_type, SourceType::Note);
    assert_eq!(doc.source_id, 1);
    assert_eq!(doc.project_id, 1);
    assert_eq!(doc.author_username, Some("alice".to_string()));
    assert!(doc.content_text.contains("[[Note]]"));
    assert!(doc.content_text.contains("source_type: note"));
    assert!(doc.content_text.contains("note_gitlab_id: 12345"));
    assert!(doc.content_text.contains("project: group/project-one"));
    assert!(doc.content_text.contains("parent_type: Issue"));
    assert!(doc.content_text.contains("parent_iid: 42"));
    assert!(doc.content_text.contains("parent_title: Fix login bug"));
    assert!(doc.content_text.contains("author: @alice"));
    assert!(doc.content_text.contains("--- Body ---"));
    assert!(
        doc.content_text
            .contains("This looks like a race condition")
    );
    assert_eq!(
        doc.title,
        Some("Note by @alice on Issue #42: Fix login bug".to_string())
    );
    assert_eq!(
        doc.url,
        Some("https://gitlab.example.com/group/project-one/-/issues/42#note_12345".to_string())
    );
}

#[test]
fn test_note_document_diffnote_with_path() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Refactor auth"),
        Some("desc"),
        "opened",
        None,
        Some("https://gitlab.example.com/group/project-one/-/issues/10"),
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note_with_type(
        &conn,
        1,
        555,
        1,
        Some("bob"),
        Some("Unused variable here"),
        1000,
        false,
        Some("src/old_auth.rs"),
        Some("src/auth.rs"),
        Some(10),
        Some(25),
        Some("DiffNote"),
        true,
        false,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("note_type: DiffNote"));
    assert!(doc.content_text.contains("path: src/auth.rs:25"));
    assert!(doc.content_text.contains("resolved: false"));
    assert_eq!(doc.paths, vec!["src/auth.rs", "src/old_auth.rs"]);
}

#[test]
fn test_note_document_inherits_parent_labels() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_label(&conn, 1, "backend");
    insert_label(&conn, 2, "api");
    link_issue_label(&conn, 1, 1);
    link_issue_label(&conn, 1, 2);
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Note body"),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc.labels, vec!["api", "backend"]);
    assert!(doc.content_text.contains("labels: api, backend"));
}

#[test]
fn test_note_document_mr_parent() {
    let conn = setup_discussion_test_db();
    insert_mr(
        &conn,
        1,
        456,
        Some("JWT Auth"),
        Some("desc"),
        Some("opened"),
        Some("johndoe"),
        Some("feature/jwt"),
        Some("main"),
        Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
    );
    insert_discussion(&conn, 1, "MergeRequest", None, Some(1));
    insert_note(
        &conn,
        1,
        200,
        1,
        Some("reviewer"),
        Some("Needs tests"),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("parent_type: MergeRequest"));
    assert!(doc.content_text.contains("parent_iid: 456"));
    assert_eq!(
        doc.title,
        Some("Note by @reviewer on MR !456: JWT Auth".to_string())
    );
}

#[test]
fn test_note_document_system_note_returns_none() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("bot"),
        Some("assigned to @alice"),
        1000,
        true,
        None,
        None,
    );

    let result = extract_note_document(&conn, 1).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_note_document_not_found() {
    let conn = setup_discussion_test_db();
    let result = extract_note_document(&conn, 999).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_note_document_orphaned_discussion() {
    let conn = setup_discussion_test_db();
    insert_discussion(&conn, 1, "Issue", None, None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Comment"),
        1000,
        false,
        None,
        None,
    );

    let result = extract_note_document(&conn, 1).unwrap();
    assert!(result.is_none());
}

#[test]
fn test_note_document_hash_deterministic() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some("Comment"),
        1000,
        false,
        None,
        None,
    );

    let doc1 = extract_note_document(&conn, 1).unwrap().unwrap();
    let doc2 = extract_note_document(&conn, 1).unwrap().unwrap();
    assert_eq!(doc1.content_hash, doc2.content_hash);
    assert_eq!(doc1.labels_hash, doc2.labels_hash);
    assert_eq!(doc1.paths_hash, doc2.paths_hash);
    assert_eq!(doc1.content_hash.len(), 64);
}

#[test]
fn test_note_document_empty_body() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        Some(""),
        1000,
        false,
        None,
        None,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("--- Body ---\n\n"));
    assert!(!doc.is_truncated);
}

#[test]
fn test_note_document_null_body() {
    let conn = setup_discussion_test_db();
    insert_issue(
        &conn,
        1,
        10,
        Some("Test"),
        Some("desc"),
        "opened",
        None,
        None,
    );
    insert_discussion(&conn, 1, "Issue", Some(1), None);
    insert_note(
        &conn,
        1,
        100,
        1,
        Some("alice"),
        None,
        1000,
        false,
        None,
        None,
    );

    let doc = extract_note_document(&conn, 1).unwrap().unwrap();
    assert!(doc.content_text.contains("--- Body ---\n\n"));
    assert!(doc.content_text.ends_with("--- Body ---\n\n"));
}
