// Copyright (C) 2025-2026  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

#![cfg(feature = "arrow")]

use std::sync::Arc;

use chrono::Utc;
use itertools::Itertools;
use osv::schema::{Affected, Event, Range, RangeType, Vulnerability};

use swh_osv::{Stats, git};

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

fn make_vulnerability(id: &str, ranges: Vec<Range>) -> Vulnerability {
    Vulnerability {
        id: id.to_string(),
        modified: chrono::Utc::now(),
        published: None,
        withdrawn: None,
        aliases: None,
        related: None,
        summary: None,
        details: None,
        severity: None,
        affected: Some(vec![Affected {
            package: None,
            severity: None,
            ranges: Some(ranges),
            versions: None,
            ecosystem_specific: None,
            database_specific: None,
        }]),
        references: None,
        credits: None,
        database_specific: None,
        upstream: None,
        schema_version: None,
    }
}

#[tokio::test(flavor = "multi_thread")]
#[test_log::test]
async fn test_list_commits() {
    let graph = Arc::new(get_10rev_linear_graph());

    let stats = Stats::default();

    let git_range_parser = git::GitRangeParser::new(Arc::clone(&graph));

    let range = Range {
        range_type: RangeType::Git,
        repo: None,
        events: vec![
            Event::Introduced("0000000000000000000000000000000000000002".to_string()),
            Event::Fixed("0000000000000000000000000000000000000003".to_string()),
        ],
    };

    let vuln = make_vulnerability("TEST-001", vec![range]);

    let result = swh_osv::vuln2commit_parquet::list_commits_affected_by_vulnerability(
        "test.json".to_string(),
        vuln,
        &git_range_parser,
        &stats,
        &None,
    )
    .unwrap();

    assert_eq!(
        result.map(|s| s.into_iter().collect::<Vec<_>>()),
        Some(vec![2])
    );
}

#[tokio::test(flavor = "multi_thread")]
#[test_log::test]
async fn test_list_commits_with_no_affected() {
    let graph = Arc::new(get_10rev_linear_graph());
    let stats = Stats::default();

    let git_range_parser = git::GitRangeParser::new(Arc::clone(&graph));

    let vuln = Vulnerability {
        id: "TEST-002".to_string(),
        modified: Utc::now(),
        published: None,
        withdrawn: None,
        aliases: None,
        related: None,
        summary: None,
        details: None,
        severity: None,
        affected: None,
        references: None,
        credits: None,
        database_specific: None,
        upstream: None,
        schema_version: None,
    };

    let result = swh_osv::vuln2commit_parquet::list_commits_affected_by_vulnerability(
        "test.json".to_string(),
        vuln,
        &git_range_parser,
        &stats,
        &None,
    )
    .unwrap();

    assert_eq!(result, None);
}

#[tokio::test(flavor = "multi_thread")]
#[test_log::test]
async fn test_list_commits_with_multiple_ranges() {
    let graph = Arc::new(get_10rev_linear_graph());

    let stats = Stats::default();

    let git_range_parser = git::GitRangeParser::new(Arc::clone(&graph));

    let range1 = Range {
        range_type: RangeType::Git,
        repo: None,
        events: vec![
            Event::Introduced("0000000000000000000000000000000000000001".to_string()),
            Event::Fixed("0000000000000000000000000000000000000002".to_string()),
        ],
    };

    let range2 = Range {
        range_type: RangeType::Git,
        repo: None,
        events: vec![
            Event::Introduced("0000000000000000000000000000000000000002".to_string()),
            Event::Fixed("0000000000000000000000000000000000000003".to_string()),
        ],
    };

    let vuln = make_vulnerability("TEST-004", vec![range1, range2]);

    let result = swh_osv::vuln2commit_parquet::list_commits_affected_by_vulnerability(
        "test.json".to_string(),
        vuln,
        &git_range_parser,
        &stats,
        &None,
    )
    .unwrap();

    assert_eq!(
        result.map(|s| s.into_iter().sorted().collect::<Vec<_>>()),
        Some(vec![1, 2])
    );
}

#[tokio::test(flavor = "multi_thread")]
#[test_log::test]
async fn test_list_commits_ignores_unknown_commits() {
    let graph = Arc::new(get_10rev_linear_graph());
    let stats = Stats::default();

    let git_range_parser = git::GitRangeParser::new(Arc::clone(&graph));

    let range = Range {
        range_type: RangeType::Git,
        repo: None,
        events: vec![
            Event::Introduced("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
            Event::Fixed("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string()),
        ],
    };

    let vuln = make_vulnerability("TEST-005", vec![range]);

    let result = swh_osv::vuln2commit_parquet::list_commits_affected_by_vulnerability(
        "test.json".to_string(),
        vuln,
        &git_range_parser,
        &stats,
        &None,
    )
    .unwrap();

    assert_eq!(
        result.map(|s| s.into_iter().collect::<Vec<_>>()),
        Some(vec![])
    );
}

#[tokio::test(flavor = "multi_thread")]
#[test_log::test]
async fn test_list_commits_with_non_git_ranges() {
    let graph = Arc::new(get_10rev_linear_graph());
    let stats = Stats::default();

    let git_range_parser = git::GitRangeParser::new(Arc::clone(&graph));

    let range = Range {
        range_type: RangeType::Semver,
        repo: None,
        events: vec![],
    };

    let vuln = make_vulnerability("TEST-006", vec![range]);

    let result = swh_osv::vuln2commit_parquet::list_commits_affected_by_vulnerability(
        "test.json".to_string(),
        vuln,
        &git_range_parser,
        &stats,
        &None,
    )
    .unwrap();

    assert_eq!(
        result.map(|s| s.into_iter().collect::<Vec<_>>()),
        Some(vec![])
    );
}
