// Copyright (C) 2025  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

use std::sync::Arc;

use anyhow::Result;
use chrono::Utc;
use dashmap::DashSet;
use dsi_progress_logger::no_logging;
use osv::schema::{Affected, Event, Range, RangeType, Vulnerability};
use rapidhash::RapidBuildHasher;
use rusqlite::Connection;

use swh_osv::{db, git};

#[path = "./graphs.rs"]
mod graphs;

/// Creates an in-memory SQLite database with test vulnerability data
fn create_test_db_with_vulnerabilities(
    vulnerabilities: Vec<(String, Vulnerability)>,
) -> Connection {
    let conn = Connection::open_in_memory().unwrap();

    // Create the vulnerabilities table
    conn.execute(
        "CREATE TABLE vulnerabilities (
            filename TEXT NOT NULL,
            doc TEXT NOT NULL
        )",
        [],
    )
    .unwrap();

    // Insert vulnerabilities
    for (filename, vuln) in vulnerabilities {
        let doc = serde_json::to_string(&vuln).unwrap();
        conn.execute(
            "INSERT INTO vulnerabilities (filename, doc) VALUES (?, ?)",
            [&filename, &doc],
        )
        .unwrap();
    }

    conn
}

/// Creates a vulnerability with a git range containing the given events
fn make_vulnerability(id: String, events: Vec<Event>) -> Vulnerability {
    Vulnerability {
        id,
        affected: Some(vec![Affected {
            ranges: Some(vec![Range {
                range_type: RangeType::Git,
                repo: None,
                events,
            }]),
            package: None,
            severity: None,
            versions: None,
            ecosystem_specific: None,
            database_specific: None,
        }]),
        modified: Utc::now(),
        published: None,
        withdrawn: None,
        aliases: None,
        related: None,
        upstream: None,
        summary: None,
        details: None,
        severity: None,
        references: None,
        credits: None,
        database_specific: None,
        schema_version: None,
    }
}

#[test]
fn test_collect_commits_empty_database() -> Result<()> {
    // Create empty database
    let conn = create_test_db_with_vulnerabilities(vec![]);

    let graph = Arc::new(graphs::get_10rev_linear_graph());

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    // Should have collected no commits
    let commits_set: Vec<usize> = commits_set.into_iter().collect();
    assert_eq!(commits_set, Vec::<usize>::new());

    Ok(())
}

#[test]
fn test_collect_commits() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create vulnerability with introduced and fixed events
    let vuln = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![
            Event::Introduced("0000000000000000000000000000000000000001".into()),
            Event::Fixed("0000000000000000000000000000000000000003".into()),
        ],
    );

    let conn = create_test_db_with_vulnerabilities(vec![("test.json".to_string(), vuln)]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    // Should have collected commits from the introduced and fixed events
    let mut commits_set: Vec<usize> = commits_set.into_iter().collect();
    commits_set.sort();
    assert_eq!(commits_set, vec![1, 3]); // rev1 (introduced), rev3 (fixed)

    Ok(())
}

#[test]
fn test_collect_commits_multiple_vulnerabilities() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create multiple vulnerabilities referencing different commits
    let vuln1 = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![
            Event::Introduced("0000000000000000000000000000000000000001".into()),
            Event::Fixed("0000000000000000000000000000000000000002".into()),
        ],
    );

    let vuln2 = make_vulnerability(
        "TEST-2025-002".to_string(),
        vec![
            Event::Introduced("0000000000000000000000000000000000000002".into()),
            Event::Fixed("0000000000000000000000000000000000000004".into()),
        ],
    );

    let conn = create_test_db_with_vulnerabilities(vec![
        ("test1.json".to_string(), vuln1),
        ("test2.json".to_string(), vuln2),
    ]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    // Should have collected all 3 commits mentioned in a vulnerability
    let mut commits_set: Vec<usize> = commits_set.into_iter().collect();
    commits_set.sort();
    assert_eq!(commits_set, vec![1, 2, 4]);

    Ok(())
}

// Checks that entire range is skipped when any commit is unknown
#[test]
fn test_collect_commits_with_unknown_commits() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create vulnerability referencing both known and unknown commits
    let vuln = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![
            Event::Introduced("0000000000000000000000000000000000000001".into()), // known
            Event::Fixed("0000000000000000000000000000000000000099".into()),      // unknown
        ],
    );

    let conn = create_test_db_with_vulnerabilities(vec![("test.json".to_string(), vuln)]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    // Should not fail, but skip the range with unknown commits
    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    let commits_set: Vec<usize> = commits_set.into_iter().collect();
    assert_eq!(commits_set, Vec::<usize>::new());

    Ok(())
}

#[test]
fn test_collect_commits_multiple_event_types() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create vulnerability with all event types
    let vuln = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![
            Event::Introduced("0000000000000000000000000000000000000001".into()),
            Event::Fixed("0000000000000000000000000000000000000002".into()),
            Event::LastAffected("0000000000000000000000000000000000000003".into()),
            Event::Limit("0000000000000000000000000000000000000004".into()),
        ],
    );

    let conn = create_test_db_with_vulnerabilities(vec![("test.json".to_string(), vuln)]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    let mut commits_set: Vec<usize> = commits_set.into_iter().collect();
    commits_set.sort();
    // introduced, fixed, last_affected, limit
    assert_eq!(commits_set, vec![1, 2, 3, 4]);

    Ok(())
}

#[test]
fn test_collect_commits_deduplicates() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create multiple vulnerabilities referencing the same commit
    let vuln1 = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![Event::Introduced(
            "0000000000000000000000000000000000000001".into(),
        )],
    );

    let vuln2 = make_vulnerability(
        "TEST-2025-002".to_string(),
        vec![Event::Fixed(
            "0000000000000000000000000000000000000001".into(),
        )],
    );

    let conn = create_test_db_with_vulnerabilities(vec![
        ("test1.json".to_string(), vuln1),
        ("test2.json".to_string(), vuln2),
    ]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    // Should have deduplicated the commit
    let mut commits_set: Vec<usize> = commits_set.into_iter().collect();
    commits_set.sort();
    assert_eq!(commits_set, vec![1]);

    Ok(())
}

#[test]
fn test_collect_commits_ignores_non_git_ranges() -> Result<()> {
    let graph = Arc::new(graphs::get_10rev_linear_graph());

    // Create vulnerability with non-git range type
    let mut vuln = make_vulnerability(
        "TEST-2025-001".to_string(),
        vec![Event::Introduced("1.0.0".into())],
    );

    // Change the range type to Semver (not Git)
    if let Some(ref mut affected) = vuln.affected
        && let Some(ref mut ranges) = affected[0].ranges
    {
        ranges[0].range_type = RangeType::Semver;
    }

    let conn = create_test_db_with_vulnerabilities(vec![("test.json".to_string(), vuln)]);

    let git_range_parser = git::GitRangeParser::new(graph.clone());
    let commits_set = DashSet::with_hasher(RapidBuildHasher::default());

    db::collect_commits_from_vulnerability_ranges(
        &conn,
        &*no_logging![],
        None,
        &git_range_parser,
        &commits_set,
    )?;

    // Should not collect any commits (only processes Git ranges)
    let commits_set: Vec<usize> = commits_set.into_iter().collect();
    assert_eq!(commits_set, Vec::<usize>::new());

    Ok(())
}
