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

use std::sync::atomic::Ordering;

use anyhow::Result;
use osv::schema::{Affected, Ecosystem, Event, Package, Range, RangeType};
use swh_graph::graph::SwhGraphWithProperties;
use swh_graph::graph_builder::{BuiltGraph, GraphBuilder};
use swh_graph::{SWHID, swhid};

use swh_osv::packages::versions::VersionType;
use swh_osv::packages::{
    MapAffectedResult, PackageNotFoundInSwh, PackageVersionInVcs, Stats, SwhAffected, SwhEvent,
    SwhRange, VersionInSwh, VersionMatch, VersionSearchResult, VersionToSwhidMapping,
    find_corresponding_source_in_vcs, find_single_package_version_in_swh, map_affected,
    map_package_version_to_swh,
};

// Helper to create a test graph with package version branches
fn create_package_graph(origin_url: &str) -> (BuiltGraph, usize) {
    let mut builder = GraphBuilder::default();

    let origin = builder
        .node(SWHID::from_origin_url(origin_url))
        .unwrap()
        .message(origin_url.as_bytes().to_vec())
        .done();

    let snp = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000010))
        .unwrap()
        .done();

    let rev_101 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000101))
        .unwrap()
        .done();

    let rev_102 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000102))
        .unwrap()
        .done();

    let dir_201 = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000201))
        .unwrap()
        .done();

    let dir_202 = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000202))
        .unwrap()
        .done();

    builder.arc(origin, snp);
    builder.snp_arc(snp, rev_101, b"1.0.0");
    builder.snp_arc(snp, rev_102, b"v1.1.0");

    builder.arc(rev_101, dir_201);
    builder.arc(rev_102, dir_202);
    builder.arc(dir_201, rev_101);
    builder.arc(dir_202, rev_102);
    builder.arc(rev_102, rev_101);

    (builder.done().unwrap(), origin)
}

// Helper to create a graph with fuzzy branch name for testing
fn create_graph_with_fuzzy_branch() -> (BuiltGraph, usize) {
    let mut builder = GraphBuilder::default();

    let origin = builder
        .node(swhid!(swh:1:ori:0000000000000000000000000000000000000001))
        .unwrap()
        .done();

    let snp = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000010))
        .unwrap()
        .done();

    let rev = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000101))
        .unwrap()
        .done();

    let dir = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000201))
        .unwrap()
        .done();

    builder.arc(origin, snp);
    // Branch name contains version but doesn't match exactly
    builder.snp_arc(snp, rev, b"refs/tags/release-1.2.3");

    builder.arc(rev, dir);
    builder.arc(dir, rev);

    (builder.done().unwrap(), origin)
}

#[test]
fn test_find_single_package_version_exact_match() {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);

    let result =
        find_single_package_version_in_swh(&graph, &[(origin, origin_url.to_string())], "1.0.0");

    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "1.0.0".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: false,
            branch_name: b"1.0.0".to_vec(),
            node: 2,
        })
    );
}

#[test]
fn test_find_single_package_version_with_v_prefix() {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);

    let result =
        find_single_package_version_in_swh(&graph, &[(origin, origin_url.to_string())], "1.1.0");

    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "1.1.0".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: false,
            branch_name: b"v1.1.0".to_vec(),
            node: 3,
        })
    );
}

#[test]
fn test_find_single_package_version_not_found() {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);

    let result =
        find_single_package_version_in_swh(&graph, &[(origin, origin_url.to_string())], "99.0.0");

    assert_eq!(
        result,
        VersionSearchResult::NotFound {
            branch_names: vec![b"1.0.0".to_vec(), b"v1.1.0".to_vec()],
        }
    );
}

#[test]
fn test_map_affected_with_package_not_in_swh() -> Result<()> {
    let (graph, _origin) = create_package_graph("https://www.npmjs.com/package/test-package");
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "nonexistent".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Semver,
            repo: None,
            events: vec![Event::Fixed("1.0.0".to_string())],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-001.json", affected)?;

    assert_eq!(
        result,
        MapAffectedResult::PackageNotInSwh(PackageNotFoundInSwh {
            tried_origin_urls: vec!["https://www.npmjs.com/package/nonexistent".to_string()]
        })
    );
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_semver_ranges.load(Ordering::Relaxed), 0);

    Ok(())
}

#[test]
fn test_map_affected_with_version_range() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Semver,
            repo: None,
            events: vec![Event::Fixed("1.0.0".to_string())],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-001.json", affected)?;

    let expected = MapAffectedResult::Found(SwhAffected {
        origins: vec![origin_url.to_string()],
        ranges: Some(vec![SwhRange {
            events: vec![SwhEvent::Fixed(VersionInSwh::MappedFromVersion(
                VersionToSwhidMapping {
                    package_version: VersionMatch {
                        version_name: "1.0.0".to_string(),
                        origin_url: origin_url.to_string(),
                        fuzzy_branch_name_match: false,
                        branch_name: b"1.0.0".to_vec(),
                        node: 2,
                    },
                    version_type: VersionType::Semver,
                    vcs: vec![],
                },
            ))],
            version_type: VersionType::Semver,
        }]),
        versions: (),
    });

    assert_eq!(result, expected);
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_semver_ranges.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_map_affected_with_git_range() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Git,
            repo: Some("https://github.com/test/repo".to_string()),
            events: vec![
                Event::Introduced("0".to_string()),
                Event::Fixed("0000000000000000000000000000000000000101".to_string()),
            ],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-002.json", affected)?;

    assert_eq!(
        result,
        MapAffectedResult::Found(SwhAffected {
            origins: vec![origin_url.to_string()],
            ranges: Some(vec![SwhRange {
                events: vec![
                    SwhEvent::Introduced(VersionInSwh::SinceTheBeginning),
                    SwhEvent::Fixed(VersionInSwh::FromGit(
                        swhid!(swh:1:rev:0000000000000000000000000000000000000101)
                    ))
                ],
                version_type: VersionType::Git
            }]),
            versions: ()
        })
    );
    assert_eq!(stats.total_git_ranges.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_map_affected_with_invalid_git_range() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Git,
            repo: Some("https://github.com/test/repo".to_string()),
            events: vec![Event::Fixed("abc123".to_string())], // not a valid commit id
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-002.json", affected)?;

    assert_eq!(
        result,
        MapAffectedResult::Found(SwhAffected {
            origins: vec![origin_url.to_string()],
            ranges: Some(vec![SwhRange {
                events: vec![],
                version_type: VersionType::Git,
            }]),
            versions: ()
        })
    );
    assert_eq!(stats.total_git_ranges.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_map_affected_with_multiple_events() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Semver,
            repo: None,
            events: vec![
                Event::Introduced("0".to_string()),
                Event::Fixed("1.0.0".to_string()),
            ],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-003.json", affected)?;

    let expected = MapAffectedResult::Found(SwhAffected {
        origins: vec![origin_url.to_string()],
        ranges: Some(vec![SwhRange {
            events: vec![
                SwhEvent::Introduced(VersionInSwh::SinceTheBeginning),
                SwhEvent::Fixed(VersionInSwh::MappedFromVersion(VersionToSwhidMapping {
                    package_version: VersionMatch {
                        version_name: "1.0.0".to_string(),
                        origin_url: origin_url.to_string(),
                        fuzzy_branch_name_match: false,
                        branch_name: b"1.0.0".to_vec(),
                        node: 2,
                    },
                    version_type: VersionType::Semver,
                    vcs: vec![],
                })),
            ],
            version_type: VersionType::Semver,
        }]),
        versions: (),
    });

    assert_eq!(result, expected);
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_semver_ranges.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_map_affected_with_multiple_ranges() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![
            Range {
                range_type: RangeType::Semver,
                repo: None,
                events: vec![
                    Event::Introduced("0".to_string()),
                    Event::Fixed("1.0.0".to_string()),
                ],
            },
            Range {
                range_type: RangeType::Semver,
                repo: None,
                events: vec![Event::Introduced("1.1.0".to_string())],
            },
        ]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-004.json", affected)?;

    let expected = MapAffectedResult::Found(SwhAffected {
        origins: vec![origin_url.to_string()],
        ranges: Some(vec![
            SwhRange {
                events: vec![
                    SwhEvent::Introduced(VersionInSwh::SinceTheBeginning),
                    SwhEvent::Fixed(VersionInSwh::MappedFromVersion(VersionToSwhidMapping {
                        package_version: VersionMatch {
                            version_name: "1.0.0".to_string(),
                            origin_url: origin_url.to_string(),
                            fuzzy_branch_name_match: false,
                            branch_name: b"1.0.0".to_vec(),
                            node: 2,
                        },
                        version_type: VersionType::Semver,
                        vcs: vec![],
                    })),
                ],
                version_type: VersionType::Semver,
            },
            SwhRange {
                events: vec![SwhEvent::Introduced(VersionInSwh::MappedFromVersion(
                    VersionToSwhidMapping {
                        package_version: VersionMatch {
                            version_name: "1.1.0".to_string(),
                            origin_url: origin_url.to_string(),
                            fuzzy_branch_name_match: false,
                            branch_name: b"v1.1.0".to_vec(),
                            node: 3,
                        },
                        version_type: VersionType::Semver,
                        vcs: vec![PackageVersionInVcs {
                            dir: graph
                                .properties()
                                .node_id(swhid!(swh:1:dir:0000000000000000000000000000000000000202))
                                .unwrap(),
                            rev: graph
                                .properties()
                                .node_id(swhid!(swh:1:rev:0000000000000000000000000000000000000102))
                                .unwrap(),
                        }],
                    },
                ))],
                version_type: VersionType::Semver,
            },
        ]),
        versions: (),
    });

    assert_eq!(result, expected);
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_semver_ranges.load(Ordering::Relaxed), 2);

    Ok(())
}

#[test]
fn test_map_affected_with_ecosystem_range() -> Result<()> {
    let origin_url = "deb://Debian/packages/openssl";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Debian(None),
            name: "openssl".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Ecosystem,
            repo: None,
            events: vec![
                Event::Introduced("0".to_string()),
                Event::Fixed("1.0.0".to_string()),
            ],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-008.json", affected)?;

    let expected = MapAffectedResult::Found(SwhAffected {
        origins: vec![origin_url.to_string()],
        ranges: Some(vec![SwhRange {
            events: vec![
                SwhEvent::Introduced(VersionInSwh::SinceTheBeginning),
                SwhEvent::Fixed(VersionInSwh::MappedFromVersion(VersionToSwhidMapping {
                    package_version: VersionMatch {
                        version_name: "1.0.0".to_string(),
                        origin_url: origin_url.to_string(),
                        fuzzy_branch_name_match: false,
                        branch_name: b"1.0.0".to_vec(),
                        node: 2,
                    },
                    version_type: VersionType::Dpkg,
                    vcs: vec![],
                })),
            ],
            version_type: VersionType::Dpkg,
        }]),
        versions: (),
    });

    assert_eq!(result, expected);
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_ecosystem_ranges.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_semver_ranges.load(Ordering::Relaxed), 0);

    Ok(())
}

#[test]
fn test_map_affected_with_package_and_git_range() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, _origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::Npm,
            name: "test-package".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Git,
            repo: Some("https://github.com/test/repo".to_string()),
            events: vec![
                Event::Introduced("0000000000000000000000000000000000000001".to_string()),
                Event::Fixed("0000000000000000000000000000000000000101".to_string()),
            ],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-009.json", affected)?;

    let expected = MapAffectedResult::Found(SwhAffected {
        origins: vec![origin_url.to_string()],
        ranges: Some(vec![SwhRange {
            events: vec![
                SwhEvent::Introduced(VersionInSwh::FromGit(
                    "swh:1:rev:0000000000000000000000000000000000000001"
                        .parse()
                        .unwrap(),
                )),
                SwhEvent::Fixed(VersionInSwh::FromGit(
                    "swh:1:rev:0000000000000000000000000000000000000101"
                        .parse()
                        .unwrap(),
                )),
            ],
            version_type: VersionType::Git,
        }]),
        versions: (),
    });

    assert_eq!(result, expected);
    assert_eq!(stats.vulns_with_packages.load(Ordering::Relaxed), 1);
    assert_eq!(stats.total_git_ranges.load(Ordering::Relaxed), 1);
    assert_eq!(stats.git_ranges_with_repo.load(Ordering::Relaxed), 1);
    assert_eq!(stats.git_ranges_with_commits.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_map_affected_unmapped_ecosystem() -> Result<()> {
    let (graph, _origin) = create_package_graph("https://crates.io/crates/swh-graph");
    let stats = Stats::default();

    let affected = Affected {
        package: Some(Package {
            ecosystem: Ecosystem::CratesIO,
            name: "swh-graph".to_string(),
            purl: None,
        }),
        ranges: Some(vec![Range {
            range_type: RangeType::Semver,
            repo: None,
            events: vec![Event::Fixed("1.0.0".to_string())],
        }]),
        severity: None,
        versions: None,
        ecosystem_specific: None,
        database_specific: None,
    };

    let result = map_affected(&graph, &stats, "TEST-003.json", affected)?;

    assert_eq!(result, MapAffectedResult::UnmappedEcosystem);
    assert_eq!(stats.packages_unmapped_ecosystem.load(Ordering::Relaxed), 1);

    Ok(())
}

#[test]
fn test_find_single_package_version_fuzzy_match() {
    let origin_url = "https://example.com/package";
    let (graph, origin) = create_graph_with_fuzzy_branch();

    let result =
        find_single_package_version_in_swh(&graph, &[(origin, origin_url.to_string())], "1.2.3");

    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "1.2.3".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: true,
            branch_name: b"refs/tags/release-1.2.3".to_vec(),
            node: 2,
        })
    );
}

#[test]
fn test_find_single_package_version_with_refs_tags_prefix() {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);

    // Should match "1.0.0" when searching with full ref path
    let result =
        find_single_package_version_in_swh(&graph, &[(origin, origin_url.to_string())], "1.0.0");
    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "1.0.0".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: false,
            branch_name: b"1.0.0".to_vec(),
            node: 2,
        })
    );
}

#[test]
fn test_find_single_package_version_multiple_origins() {
    let mut builder = GraphBuilder::default();

    let origin1 = builder
        .node(swhid!(swh:1:ori:0000000000000000000000000000000000000001))
        .unwrap()
        .done();
    let origin2 = builder
        .node(swhid!(swh:1:ori:0000000000000000000000000000000000000002))
        .unwrap()
        .done();

    let snp1 = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000010))
        .unwrap()
        .done();
    let snp2 = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000011))
        .unwrap()
        .done();

    let rev1 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000101))
        .unwrap()
        .done();
    let rev2 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000102))
        .unwrap()
        .done();

    let dir1 = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000201))
        .unwrap()
        .done();
    let dir2 = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000202))
        .unwrap()
        .done();

    builder.arc(origin1, snp1);
    builder.arc(origin2, snp2);
    builder.snp_arc(snp1, rev1, b"1.0.0");
    builder.snp_arc(snp2, rev2, b"2.0.0");
    builder.arc(rev1, dir1);
    builder.arc(rev2, dir2);

    let graph = builder.done().unwrap();

    let origin1_url = "https://example.com/package1".to_string();
    let origin2_url = "https://example.com/package2".to_string();

    // Should find version 1.0.0 in origin1
    let result = find_single_package_version_in_swh(
        &graph,
        &[
            (origin1, origin1_url.clone()),
            (origin2, origin2_url.clone()),
        ],
        "1.0.0",
    );
    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "1.0.0".to_string(),
            origin_url: origin1_url.clone(),
            fuzzy_branch_name_match: false,
            branch_name: b"1.0.0".to_vec(),
            node: 4,
        })
    );

    // Should find version 2.0.0 in origin2
    let result = find_single_package_version_in_swh(
        &graph,
        &[(origin1, origin1_url), (origin2, origin2_url.clone())],
        "2.0.0",
    );
    assert_eq!(
        result,
        VersionSearchResult::Found(VersionMatch {
            version_name: "2.0.0".to_string(),
            origin_url: origin2_url,
            fuzzy_branch_name_match: false,
            branch_name: b"2.0.0".to_vec(),
            node: 5,
        })
    );
}

#[test]
fn test_map_package_version_to_swh_special_version_zero() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let result = map_package_version_to_swh(
        &graph,
        &[(origin, origin_url.to_string())],
        &"0".to_string(),
        &stats,
        &Ecosystem::Npm,
        VersionType::Semver,
    )?;

    assert_eq!(result, VersionInSwh::SinceTheBeginning);

    Ok(())
}

#[test]
fn test_map_package_version_to_swh_version_found() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let result = map_package_version_to_swh(
        &graph,
        &[(origin, origin_url.to_string())],
        &"1.0.0".to_string(),
        &stats,
        &Ecosystem::Npm,
        VersionType::Semver,
    )?;

    let expected = VersionInSwh::MappedFromVersion(VersionToSwhidMapping {
        package_version: VersionMatch {
            version_name: "1.0.0".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: false,
            branch_name: b"1.0.0".to_vec(),
            node: 2,
        },
        version_type: VersionType::Semver,
        vcs: vec![],
    });
    assert_eq!(result, expected);

    assert_eq!(
        stats
            .found_sure_package_version_in_swh_by_ecosystem
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );

    Ok(())
}

#[test]
fn test_map_package_version_to_swh_version_not_found() -> Result<()> {
    let origin_url = "https://www.npmjs.com/package/test-package";
    let (graph, origin) = create_package_graph(origin_url);
    let stats = Stats::default();

    let result = map_package_version_to_swh(
        &graph,
        &[(origin, origin_url.to_string())],
        &"99.0.0".to_string(),
        &stats,
        &Ecosystem::Npm,
        VersionType::Semver,
    )?;

    // Check that version not found includes search info
    match result {
        VersionInSwh::VersionNotFound(search_info) => {
            assert_eq!(search_info.tried_origin_urls, vec![origin_url.to_string()]);
            assert_eq!(search_info.branch_names, vec!["1.0.0", "v1.1.0"]);
        }
        _ => panic!("Expected VersionNotFound, got {:?}", result),
    }

    assert_eq!(
        stats
            .unfound_package_version_in_swh_by_ecosystem
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );

    Ok(())
}

#[test]
fn test_map_package_version_to_swh_fuzzy_match() -> Result<()> {
    let origin_url = "https://example.com/package";
    let (graph, origin) = create_graph_with_fuzzy_branch();
    let stats = Stats::default();

    let result = map_package_version_to_swh(
        &graph,
        &[(origin, origin_url.to_string())],
        &"1.2.3".to_string(),
        &stats,
        &Ecosystem::Npm,
        VersionType::Semver,
    )?;

    let expected = VersionInSwh::MappedFromVersion(VersionToSwhidMapping {
        package_version: VersionMatch {
            version_name: "1.2.3".to_string(),
            origin_url: origin_url.to_string(),
            fuzzy_branch_name_match: true,
            branch_name: b"refs/tags/release-1.2.3".to_vec(),
            node: 2,
        },
        version_type: VersionType::Semver,
        vcs: vec![],
    });
    assert_eq!(result, expected);

    assert_eq!(
        stats
            .found_fuzzy_package_version_in_swh_by_ecosystem
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );

    Ok(())
}

#[test]
fn test_find_corresponding_source_in_vcs() -> Result<()> {
    let mut builder = GraphBuilder::default();

    let origin = builder
        .node(swhid!(swh:1:ori:0000000000000000000000000000000000000001))
        .unwrap()
        .done();

    let snp = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000010))
        .unwrap()
        .done();

    let rev_child = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000101))
        .unwrap()
        .done();

    let rev_parent = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000102))
        .unwrap()
        .done();

    let dir = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000201))
        .unwrap()
        .done();

    builder.arc(origin, snp);
    builder.snp_arc(snp, rev_child, b"1.0.0");

    builder.arc(rev_child, dir);
    builder.arc(rev_child, rev_parent);
    builder.arc(dir, rev_child);

    let graph = builder.done().unwrap();
    let stats = Stats::default();

    let vcs_matches = find_corresponding_source_in_vcs(&graph, &stats, &Ecosystem::Npm, rev_child)?;

    let expected = vec![PackageVersionInVcs {
        dir,
        rev: rev_child,
    }];
    assert_eq!(vcs_matches, expected);
    assert_eq!(
        stats
            .package_dirs_with_revisions
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );
    assert_eq!(
        stats
            .package_version_dir_in_vcs_by_ecosystem
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );

    Ok(())
}

/// Finds a revision but it does not have a parent, so it is either a root revision or a synthetic
/// revision generated by old SWH loaders
#[test]
fn test_find_corresponding_source_in_vcs_missing_parent() -> Result<()> {
    let mut builder = GraphBuilder::default();

    let origin = builder
        .node(swhid!(swh:1:ori:0000000000000000000000000000000000000001))
        .unwrap()
        .done();

    let snp = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000010))
        .unwrap()
        .done();

    let rev = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000101))
        .unwrap()
        .done();

    let dir = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000201))
        .unwrap()
        .done();

    builder.arc(origin, snp);
    builder.snp_arc(snp, rev, b"1.0.0");
    builder.arc(rev, dir);
    builder.arc(dir, rev);

    let graph = builder.done().unwrap();
    let stats = Stats::default();

    let vcs_matches = find_corresponding_source_in_vcs(&graph, &stats, &Ecosystem::Npm, rev)?;

    assert_eq!(vcs_matches, vec![]);
    assert_eq!(
        stats
            .package_version_dir_not_in_vcs_by_ecosystem
            .get("Npm")
            .unwrap()
            .load(Ordering::Relaxed),
        1
    );

    Ok(())
}
