use super::*;
use crate::core::db::{create_connection, run_migrations};
use std::path::Path;

fn setup_test_db() -> rusqlite::Connection {
    let conn = create_connection(Path::new(":memory:")).unwrap();
    run_migrations(&conn).unwrap();
    conn
}

fn seed_project(conn: &rusqlite::Connection) -> i64 {
    conn.execute(
        "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
         VALUES (1, 100, 'group/repo', 'https://gitlab.example.com/group/repo', 1000, 2000)",
        [],
    )
    .unwrap();
    1
}

fn insert_mr(
    conn: &rusqlite::Connection,
    id: i64,
    iid: i64,
    title: &str,
    state: &str,
    merged_at: Option<i64>,
) {
    conn.execute(
        "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username, \
         created_at, updated_at, last_seen_at, source_branch, target_branch, merged_at, web_url)
         VALUES (?1, ?2, ?3, 1, ?4, ?5, 'dev', 1000, 2000, 2000, 'feature', 'main', ?6, \
         'https://gitlab.example.com/group/repo/-/merge_requests/' || ?3)",
        rusqlite::params![id, 300 + id, iid, title, state, merged_at],
    )
    .unwrap();
}

fn insert_file_change(
    conn: &rusqlite::Connection,
    mr_id: i64,
    old_path: Option<&str>,
    new_path: &str,
    change_type: &str,
) {
    conn.execute(
        "INSERT INTO mr_file_changes (merge_request_id, project_id, old_path, new_path, change_type)
         VALUES (?1, 1, ?2, ?3, ?4)",
        rusqlite::params![mr_id, old_path, new_path, change_type],
    )
    .unwrap();
}

fn insert_entity_ref(
    conn: &rusqlite::Connection,
    source_type: &str,
    source_id: i64,
    target_type: &str,
    target_id: i64,
    ref_type: &str,
) {
    conn.execute(
        "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id, \
         target_entity_type, target_entity_id, reference_type, source_method, created_at)
         VALUES (1, ?1, ?2, ?3, ?4, ?5, 'api', 1000)",
        rusqlite::params![source_type, source_id, target_type, target_id, ref_type],
    )
    .unwrap();
}

fn insert_issue(conn: &rusqlite::Connection, id: i64, iid: i64, title: &str, state: &str) {
    conn.execute(
        "INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, \
         last_seen_at, web_url)
         VALUES (?1, ?2, 1, ?3, ?4, ?5, 1000, 2000, 2000, \
         'https://gitlab.example.com/group/repo/-/issues/' || ?3)",
        rusqlite::params![id, 400 + id, iid, title, state],
    )
    .unwrap();
}

fn insert_discussion_and_note(
    conn: &rusqlite::Connection,
    discussion_id: i64,
    mr_id: i64,
    note_id: i64,
    author: &str,
    body: &str,
    position_new_path: Option<&str>,
) {
    conn.execute(
        "INSERT INTO discussions (id, gitlab_discussion_id, project_id, merge_request_id, \
         noteable_type, last_seen_at)
         VALUES (?1, 'disc-' || ?1, 1, ?2, 'MergeRequest', 2000)",
        rusqlite::params![discussion_id, mr_id],
    )
    .unwrap();
    conn.execute(
        "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, \
         is_system, created_at, updated_at, last_seen_at, position_new_path)
         VALUES (?1, ?2, ?3, 1, ?4, ?5, 0, 1500, 1500, 2000, ?6)",
        rusqlite::params![
            note_id,
            500 + note_id,
            discussion_id,
            author,
            body,
            position_new_path
        ],
    )
    .unwrap();
}

#[test]
fn test_trace_empty_file() {
    let conn = setup_test_db();
    seed_project(&conn);

    let result = run_trace(&conn, Some(1), "src/nonexistent.rs", false, false, 10).unwrap();
    assert!(result.trace_chains.is_empty());
    assert_eq!(result.resolved_paths, ["src/nonexistent.rs"]);
}

#[test]
fn test_trace_finds_mr() {
    let conn = setup_test_db();
    seed_project(&conn);
    insert_mr(&conn, 1, 10, "Add auth module", "merged", Some(3000));
    insert_file_change(&conn, 1, None, "src/auth.rs", "added");

    let result = run_trace(&conn, Some(1), "src/auth.rs", false, false, 10).unwrap();
    assert_eq!(result.trace_chains.len(), 1);

    let chain = &result.trace_chains[0];
    assert_eq!(chain.mr_iid, 10);
    assert_eq!(chain.mr_title, "Add auth module");
    assert_eq!(chain.mr_state, "merged");
    assert_eq!(chain.change_type, "added");
    assert!(chain.merged_at_iso.is_some());
}

#[test]
fn test_trace_follows_renames() {
    let conn = setup_test_db();
    seed_project(&conn);

    // MR 1: added old_auth.rs
    insert_mr(&conn, 1, 10, "Add old auth", "merged", Some(1000));
    insert_file_change(&conn, 1, None, "src/old_auth.rs", "added");

    // MR 2: renamed old_auth.rs -> auth.rs
    insert_mr(&conn, 2, 11, "Rename auth", "merged", Some(2000));
    insert_file_change(&conn, 2, Some("src/old_auth.rs"), "src/auth.rs", "renamed");

    // Query auth.rs with follow_renames -- should find both MRs
    let result = run_trace(&conn, Some(1), "src/auth.rs", true, false, 10).unwrap();

    assert!(result.renames_followed);
    assert!(
        result
            .resolved_paths
            .contains(&"src/old_auth.rs".to_string())
    );
    assert!(result.resolved_paths.contains(&"src/auth.rs".to_string()));
    // MR 2 touches auth.rs (new_path), MR 1 touches old_auth.rs (new_path in its row)
    assert_eq!(result.trace_chains.len(), 2);
}

#[test]
fn test_trace_links_issues() {
    let conn = setup_test_db();
    seed_project(&conn);
    insert_mr(&conn, 1, 10, "Fix login bug", "merged", Some(3000));
    insert_file_change(&conn, 1, None, "src/login.rs", "modified");
    insert_issue(&conn, 1, 42, "Login broken on mobile", "closed");
    insert_entity_ref(&conn, "merge_request", 1, "issue", 1, "closes");

    let result = run_trace(&conn, Some(1), "src/login.rs", false, false, 10).unwrap();
    assert_eq!(result.trace_chains.len(), 1);
    assert_eq!(result.trace_chains[0].issues.len(), 1);

    let issue = &result.trace_chains[0].issues[0];
    assert_eq!(issue.iid, 42);
    assert_eq!(issue.title, "Login broken on mobile");
    assert_eq!(issue.reference_type, "closes");
}

#[test]
fn test_trace_limits_chains() {
    let conn = setup_test_db();
    seed_project(&conn);

    for i in 1..=3 {
        insert_mr(
            &conn,
            i,
            10 + i,
            &format!("MR {i}"),
            "merged",
            Some(1000 * i),
        );
        insert_file_change(&conn, i, None, "src/shared.rs", "modified");
    }

    let result = run_trace(&conn, Some(1), "src/shared.rs", false, false, 1).unwrap();
    assert_eq!(result.trace_chains.len(), 1);
}

#[test]
fn test_trace_no_follow_renames() {
    let conn = setup_test_db();
    seed_project(&conn);

    // MR 1: added old_name.rs
    insert_mr(&conn, 1, 10, "Add old file", "merged", Some(1000));
    insert_file_change(&conn, 1, None, "src/old_name.rs", "added");

    // MR 2: renamed old_name.rs -> new_name.rs
    insert_mr(&conn, 2, 11, "Rename file", "merged", Some(2000));
    insert_file_change(
        &conn,
        2,
        Some("src/old_name.rs"),
        "src/new_name.rs",
        "renamed",
    );

    // Without follow_renames -- should only find MR 2 (new_path = new_name.rs)
    let result = run_trace(&conn, Some(1), "src/new_name.rs", false, false, 10).unwrap();
    assert_eq!(result.resolved_paths, ["src/new_name.rs"]);
    assert!(!result.renames_followed);
    assert_eq!(result.trace_chains.len(), 1);
    assert_eq!(result.trace_chains[0].mr_iid, 11);
}

#[test]
fn test_trace_includes_discussions() {
    let conn = setup_test_db();
    seed_project(&conn);
    insert_mr(&conn, 1, 10, "Refactor auth", "merged", Some(3000));
    insert_file_change(&conn, 1, None, "src/auth.rs", "modified");
    insert_discussion_and_note(
        &conn,
        1,
        1,
        1,
        "reviewer",
        "This function should handle the error case.",
        Some("src/auth.rs"),
    );

    let result = run_trace(&conn, Some(1), "src/auth.rs", false, true, 10).unwrap();
    assert_eq!(result.trace_chains.len(), 1);
    assert_eq!(result.trace_chains[0].discussions.len(), 1);

    let disc = &result.trace_chains[0].discussions[0];
    assert_eq!(disc.author_username, "reviewer");
    assert!(disc.body.contains("error case"));
    assert_eq!(disc.mr_iid, 10);
}
