use immich_lib::models::DuplicateGroup;
use immich_lib::scoring::MetadataConflict;
use immich_lib::DuplicateAnalysis;
fn load_recorded_duplicates() -> Vec<DuplicateGroup> {
let json = include_str!("fixtures/recorded/duplicates.json");
serde_json::from_str(json).expect("Failed to parse recorded duplicates")
}
fn find_group_by_filename<'a>(
groups: &'a [DuplicateGroup],
filename: &str,
) -> Option<&'a DuplicateGroup> {
groups.iter().find(|g| {
g.assets.iter().any(|a| a.original_file_name == filename)
})
}
mod winner_selection {
use super::*;
#[test]
fn test_w1_clear_dimension_winner() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w1_large.jpg")
.expect("W1 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "w1_large.jpg",
"W1: Larger dimensions should win");
}
#[test]
fn test_w2_same_dimensions_different_size() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w2_a.jpg")
.expect("W2 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(
analysis.winner.filename == "w2_a.jpg" ||
analysis.winner.filename == "w2_b.jpg",
"W2: One of the files should win"
);
}
#[test]
fn test_w3_identical_files() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w3_a.jpg")
.expect("W3 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(
analysis.winner.filename == "w3_a.jpg" ||
analysis.winner.filename == "w3_b.jpg",
"W3: One of the files should win"
);
}
#[test]
fn test_w4_one_with_dimensions() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w4_with_dims.jpg")
.expect("W4 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "w4_with_dims.jpg",
"W4: File with dimensions should win");
}
#[test]
fn test_w5_larger_dimensions_wins() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w5_with_dims.jpg")
.expect("W5 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "w5_no_dims.jpg",
"W5: Larger dimensions should win (600x400 vs 594x396)");
}
#[test]
fn test_w6_no_dimensions() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w6_a.jpg")
.expect("W6 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(
analysis.winner.filename == "w6_a.jpg" ||
analysis.winner.filename == "w6_b.jpg",
"W6: One of the files should win"
);
}
#[test]
fn test_w7_three_duplicates() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w7_large.jpg")
.expect("W7 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "w7_large.jpg",
"W7: Largest should win");
assert_eq!(analysis.losers.len(), 2, "W7: Should have 2 losers");
}
#[test]
fn test_w8_same_pixels_different_aspect() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "w8_wide.jpg")
.expect("W8 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(
analysis.winner.filename == "w8_wide.jpg" ||
analysis.winner.filename == "w8_tall.jpg",
"W8: One of the files should win"
);
}
}
mod conflict_detection {
use super::*;
fn has_gps_conflict(analysis: &DuplicateAnalysis) -> bool {
analysis.conflicts.iter().any(|c| matches!(c, MetadataConflict::Gps { .. }))
}
fn has_timezone_conflict(analysis: &DuplicateAnalysis) -> bool {
analysis.conflicts.iter().any(|c| matches!(c, MetadataConflict::Timezone { .. }))
}
fn has_camera_conflict(analysis: &DuplicateAnalysis) -> bool {
analysis.conflicts.iter().any(|c| matches!(c, MetadataConflict::CameraInfo { .. }))
}
fn has_capture_time_conflict(analysis: &DuplicateAnalysis) -> bool {
analysis.conflicts.iter().any(|c| matches!(c, MetadataConflict::CaptureTime { .. }))
}
#[test]
fn test_f1_gps_conflict_london_vs_paris() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f1_london.jpg")
.expect("F1 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(has_gps_conflict(&analysis),
"F1: Should detect GPS conflict (London vs Paris)");
}
#[test]
fn test_f2_gps_within_threshold_no_conflict() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f2_pos_a.jpg")
.expect("F2 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(!has_gps_conflict(&analysis),
"F2: Should NOT detect GPS conflict (within threshold)");
}
#[test]
fn test_f3_timezone_conflict() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f3_tz_a.jpg")
.expect("F3 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(has_timezone_conflict(&analysis),
"F3: Should detect timezone conflict");
}
#[test]
fn test_f4_camera_conflict() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f4_canon.jpg")
.expect("F4 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(has_camera_conflict(&analysis),
"F4: Should detect camera conflict (Canon vs Nikon)");
}
#[test]
fn test_f5_capture_time_conflict() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f5_morning.jpg")
.expect("F5 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(has_capture_time_conflict(&analysis),
"F5: Should detect capture time conflict");
}
#[test]
fn test_f6_multiple_conflicts() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f6_a.jpg")
.expect("F6 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(has_gps_conflict(&analysis), "F6: Should have GPS conflict");
assert!(has_camera_conflict(&analysis), "F6: Should have camera conflict");
assert!(has_timezone_conflict(&analysis), "F6: Should have timezone conflict");
}
#[test]
fn test_f7_no_conflicts() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "f7_a.jpg")
.expect("F7 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert!(analysis.conflicts.is_empty(),
"F7: Should have no conflicts, got {:?}", analysis.conflicts);
}
}
mod consolidation {
use super::*;
#[test]
fn test_c1_winner_no_gps_loser_has_gps() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "c1_winner_no_gps.jpg")
.expect("C1 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "c1_winner_no_gps.jpg");
let loser = analysis.losers.first().expect("C1 should have a loser");
assert!(loser.score.gps > 0, "C1: Loser should have GPS metadata");
}
#[test]
fn test_c3_winner_no_desc_loser_has_desc() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "c3_winner_no_desc.jpg")
.expect("C3 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "c3_winner_no_desc.jpg");
}
#[test]
fn test_c4_winner_bare_loser_rich() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "c4_winner_bare.jpg")
.expect("C4 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "c4_winner_bare.jpg");
let loser = analysis.losers.first().expect("C4 should have a loser");
assert!(loser.score.total > analysis.winner.score.total,
"C4: Loser should have more metadata than winner");
}
#[test]
fn test_c6_multiple_losers_contribute() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "c6_winner.jpg")
.expect("C6 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "c6_winner.jpg");
assert_eq!(analysis.losers.len(), 2, "C6: Should have 2 losers");
}
#[test]
fn test_c8_winner_already_complete() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "c8_winner_full.jpg")
.expect("C8 group not found");
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "c8_winner_full.jpg");
let loser = analysis.losers.first().expect("C8 should have a loser");
assert!(analysis.winner.score.total >= loser.score.total,
"C8: Winner should have at least as much metadata as loser");
}
}
mod edge_cases {
use super::*;
#[test]
fn test_x1_single_asset_not_in_duplicates() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "x1_single.jpg");
assert!(group.is_none(),
"X1: Single asset should NOT appear in duplicate groups");
}
#[test]
fn test_x2_large_group() {
let groups = load_recorded_duplicates();
let group = find_group_by_filename(&groups, "x2_dup_00.jpg");
if let Some(g) = group {
assert!(g.assets.len() >= 2, "X2: If found, should have multiple assets");
}
}
#[test]
fn test_x3_large_file_wins() {
let groups = load_recorded_duplicates();
if let Some(group) = find_group_by_filename(&groups, "x3_large.jpg") {
let analysis = DuplicateAnalysis::from_group(group);
assert_eq!(analysis.winner.filename, "x3_large.jpg",
"X3: Larger file should win on dimensions");
}
}
#[test]
fn test_recorded_fixture_loads() {
let groups = load_recorded_duplicates();
assert!(!groups.is_empty(), "Should have recorded duplicate groups");
for group in &groups {
assert!(!group.duplicate_id.is_empty(), "Groups should have IDs");
assert!(!group.assets.is_empty(), "Groups should have assets");
for asset in &group.assets {
assert!(!asset.id.is_empty(), "Assets should have IDs");
assert!(!asset.original_file_name.is_empty(), "Assets should have filenames");
}
}
}
}