package web import ( "music-server/internal/backend" "strings" "testing" ) // TestCleanTerm tests that search terms are cleaned correctly func TestCleanTerm(t *testing.T) { tests := []struct { input string expected string }{ // CleanSearchTermForMatch removes spaces, replaces specific chars, and lowercases {"MGS4", "mgs4"}, {"RE2", "re2"}, {"FFVII", "ffvii"}, {"Ys", "ys"}, {" test ", "test"}, {"Test 123", "test123"}, {"A+B", "aplusb"}, {"é", "e"}, // Note: CleanSearchTermForMatch does NOT remove apostrophes, it only replaces "'n" with "and" {"It's", "it's"}, // But it does replace "'n" with "and" {"I'n", "iand"}, {"", ""}, } for _, tt := range tests { result := backend.CleanSearchTermForMatch(tt.input) if result != tt.expected { t.Errorf("CleanSearchTermForMatch(%q) = %q, want %q", tt.input, result, tt.expected) } } } // searchWithDataHelper is a helper to create backend.SoundtrackData from original names func searchWithDataHelper(originals []string) []backend.SoundtrackData { data := make([]backend.SoundtrackData, len(originals)) for i, orig := range originals { data[i] = backend.SoundtrackData{ Original: orig, Lowercase: strings.ToLower(orig), Cleaned: backend.CleanSearchTermForMatch(orig), } } return data } // TestNormalSearch tests the normal search function func TestNormalSearch(t *testing.T) { // Set up comprehensive test data with all user-provided soundtracks testData := searchWithDataHelper([]string{ "A Bird Story", "Actraiser", "Actraiser 2", "Bangai-O Spirits", "Ciconia When they Cry", "Civilization IV", "Civilization VI", "Clair Obscur - Expedition 33", "Club House Games (42 All-Time Classics)", "Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv)", "Final Fantasy VII Crisis Core", "Final Fantasy VII Rebirth", "Final Fantasy VII Remake", "Final Fantasy VII", "Joe & Mac", "Judgment", "Kingdom Hearts", "Kingdom Hearts Birth By Sleep", "Kingdom Hearts II", "Kingdom Hearts III", "Lemmings (SNES + Mega Drive)", "Lemmings 3D", "Mega Man X4", "Metal Gear Rising - Revengeance", "Metal Gear Solid (+Twin Snakes)", "Metal Gear Solid 2 - Sons of Liberty", "Metal Gear Solid 3 - Snake Eater", "Metal Gear Solid 4 - Guns of the Patriots", "Metroid Prime", "Octopath Traveler", "Octopath Traveler 2", "Ori and the Blind Forest", "Ori and the Will of the Wisps", "OutRun 2006 - Coast 2 Coast", "Paper Mario - The Thousand Year Door", "Pokémon HeartGold & SoulSilver", "Resident Evil 2 (Remake)", "Shenmue", "Super Mario Galaxy", "Trails in the Sky FC (Sora no Kiseki FC)", "Trails in the Sky SC (Sora no Kiseki SC)", "Turrican II - The Final Fight", "Umineko no Naku Koro ni", "Umineko no Naku Koro ni Chiru", "VVVVVV", "YS VIII - Lacrimosa of DANA", "Yakuza 0", "Ys (Chronicles)", "Ys - Memories of Celceta", "Ys - The Ark of Napishtim", "Ys II (Chronicles)", "Ys III - Wanderers from Ys (+The Oath in Felghana)", "Ys IV - The Dawn of Ys", "Ys Origin", "Ys Seven", "Ys X Nordics", "Call of Duty: Modern Warfare", "Half-Life 3", }) // Test cases covering all match types tests := []struct { query string wantFound string }{ // Exact substring matches {"Metal", "Metal Gear Solid 4 - Guns of the Patriots"}, {"Resident", "Resident Evil 2 (Remake)"}, {"Final", "Final Fantasy VII"}, {"Ys", "Ys III - Wanderers from Ys (+The Oath in Felghana)"}, // Contains matches (on cleaned terms) {"gotp", "Metal Gear Solid 4 - Guns of the Patriots"}, {"mgs", "Metal Gear Solid 4 - Guns of the Patriots"}, {"kh", "Kingdom Hearts"}, {"mg", "Metal Gear Solid 4 - Guns of the Patriots"}, // Regex matches {"mgs4go", "Metal Gear Solid 4 - Guns of the Patriots"}, {"re2r", "Resident Evil 2 (Remake)"}, {"ffv", "Final Fantasy VII"}, {"yiwfyt", "Ys III - Wanderers from Ys (+The Oath in Felghana)"}, {"codmw", "Call of Duty: Modern Warfare"}, {"mgs4gop", "Metal Gear Solid 4 - Guns of the Patriots"}, } for _, tt := range tests { t.Run(tt.query, func(t *testing.T) { results := searchWithData(tt.query, testData) // Check that the expected item is in the results found := false for _, result := range results { if result == tt.wantFound { found = true break } } if !found { t.Errorf("searchWithData(%q, ...) did not find expected item %q, got %v", tt.query, tt.wantFound, results) } }) } } // TestSearchOrdering tests that Pass 1 matches come before Pass 2 matches func TestSearchOrdering(t *testing.T) { // "MG" matches Pass 1 (exact substring on original) // "Metal Gear" matches Pass 2 (contains on cleaned) but NOT Pass 1 (no "mg" substring in "metal gear") testData := searchWithDataHelper([]string{ "Metal Gear", "MG", }) results := searchWithData("mg", testData) // Find indices indexPass1 := -1 indexPass2 := -1 for i, result := range results { if result == "MG" { indexPass1 = i } if result == "Metal Gear" { indexPass2 = i } } // Pass 1 match (MG) should come before Pass 2 match (Metal Gear) if indexPass1 >= 0 && indexPass2 >= 0 && indexPass1 >= indexPass2 { t.Errorf("Expected Pass 1 match 'MG' to come before Pass 2 match 'Metal Gear', but got order: %v", results) } } // Note: The tests above verify: // 1. clean_term - the term cleaning logic // 2. search - the complete normal search with exact, contains, and regex matching // 3. ordering - exact matches come before contains matches