import XCTest @testable import MixBoard final class GroupTemplateResolverTests: XCTestCase { private func makeTrack( artist: String = "Test Artist", album: String = "Test Album", genre: String = "Electronic", filePath: String = "Music/subfolder/song.mp3", fileFormat: String = "MP3", bpm: Double? = 128.0, musicalKey: String? = "Am", year: Int? = 2023 ) -> Track { let track = Track(title: "Test Song", artist: artist, album: album, genre: genre, filePath: filePath, fileFormat: fileFormat) track.bpm = bpm track.musicalKey = musicalKey track.year = year return track } // MARK: - Basic Placeholder Resolution func testResolveArtistTemplate() { let track = makeTrack(artist: "Raekwon") let result = GroupTemplateResolver.resolve(template: "{Artist}", for: track) XCTAssertEqual(result, "Raekwon") } func testResolveAlbumTemplate() { let track = makeTrack(album: "Only Built 4 Cuban Linx") let result = GroupTemplateResolver.resolve(template: "{Album}", for: track) XCTAssertEqual(result, "Only Built 4 Cuban Linx") } func testResolveGenreTemplate() { let track = makeTrack(genre: "Hip-Hop") let result = GroupTemplateResolver.resolve(template: "{Genre}", for: track) XCTAssertEqual(result, "Hip-Hop") } func testResolveDateTemplate() { let track = makeTrack(year: 1995) let result = GroupTemplateResolver.resolve(template: "{Date}", for: track) XCTAssertEqual(result, "1995") } func testResolveFormatTemplate() { let track = makeTrack(fileFormat: "FLAC") let result = GroupTemplateResolver.resolve(template: "{Format}", for: track) XCTAssertEqual(result, "FLAC") } func testResolveKeyTemplate() { let track = makeTrack(musicalKey: "Cm") let result = GroupTemplateResolver.resolve(template: "{Key}", for: track) XCTAssertEqual(result, "Cm") } // MARK: - Folder Resolution func testResolveFolderTemplate() { let track = makeTrack(filePath: "Music/Hip-Hop/Raekwon/track.mp3") let result = GroupTemplateResolver.resolve(template: "{Folder}", for: track) XCTAssertEqual(result, "Raekwon") } func testResolveFolderRootPath() { let track = makeTrack(filePath: "song.mp3") let result = GroupTemplateResolver.resolve(template: "{Folder}", for: track) XCTAssertEqual(result, "Root") } // MARK: - BPM Range func testResolveBPMRange() { let track = makeTrack(bpm: 125.0) let result = GroupTemplateResolver.resolve(template: "{BPM}", for: track) XCTAssertEqual(result, "120-130 BPM") } func testResolveBPMRangeExact() { let track = makeTrack(bpm: 130.0) let result = GroupTemplateResolver.resolve(template: "{BPM}", for: track) XCTAssertEqual(result, "130-140 BPM") } func testResolveBPMNil() { let track = makeTrack(bpm: nil) let result = GroupTemplateResolver.resolve(template: "{BPM}", for: track) XCTAssertEqual(result, "No BPM") } // MARK: - Compound Templates func testResolveAlbumDateTemplate() { let track = makeTrack(album: "Madvillainy", year: 2004) let result = GroupTemplateResolver.resolve(template: "{Album} ({Date})", for: track) XCTAssertEqual(result, "Madvillainy (2004)") } func testResolveArtistAlbumTemplate() { let track = makeTrack(artist: "MF DOOM", album: "Operation: Doomsday") let result = GroupTemplateResolver.resolve(template: "{Artist} — {Album}", for: track) XCTAssertEqual(result, "MF DOOM — Operation: Doomsday") } // MARK: - Missing/Empty Data Handling func testResolveEmptyTemplate() { let track = makeTrack() let result = GroupTemplateResolver.resolve(template: "", for: track) XCTAssertEqual(result, "") } func testResolveEmptyArtist() { let track = makeTrack(artist: "") let result = GroupTemplateResolver.resolve(template: "{Artist}", for: track) XCTAssertEqual(result, "Unknown Artist") } func testResolveEmptyAlbum() { let track = makeTrack(album: "") let result = GroupTemplateResolver.resolve(template: "{Album}", for: track) XCTAssertEqual(result, "Unknown Album") } func testResolveEmptyGenre() { let track = makeTrack(genre: "") let result = GroupTemplateResolver.resolve(template: "{Genre}", for: track) XCTAssertEqual(result, "Unknown Genre") } func testResolveEmptyFormat() { let track = makeTrack(fileFormat: "") let result = GroupTemplateResolver.resolve(template: "{Format}", for: track) XCTAssertEqual(result, "Unknown") } func testResolveNilKey() { let track = makeTrack(musicalKey: nil) let result = GroupTemplateResolver.resolve(template: "{Key}", for: track) XCTAssertEqual(result, "Unknown Key") } func testResolveNilDateCleansUpBrackets() { let track = makeTrack(album: "Some Album", year: nil) let result = GroupTemplateResolver.resolve(template: "{Album} ({Date})", for: track) // Empty parens should be cleaned up XCTAssertEqual(result, "Some Album") } func testResolveUnknownArtistCleanup() { let track = makeTrack(artist: "") let result = GroupTemplateResolver.resolve(template: "{Artist} — {Album}", for: track) // " — Unknown Artist" should be stripped — but since Artist is first, it stays // The cleanup rule is " — Unknown Artist" specifically XCTAssertFalse(result.isEmpty) } func testResolveAllEmptyFieldsReturnsUngrouped() { let track = Track(title: "Bare", filePath: "song.mp3") // With all empty, the resolved string may end up blank after cleanup let result = GroupTemplateResolver.resolve(template: "{Date}", for: track) // Year is nil → Date resolves to "" → template becomes "" → should return "Ungrouped" XCTAssertEqual(result, "Ungrouped") } // MARK: - Presets & Placeholders func testPresetsExist() { XCTAssertFalse(GroupTemplateResolver.presets.isEmpty) XCTAssertGreaterThanOrEqual(GroupTemplateResolver.presets.count, 8) } func testPresetsContainNoGrouping() { let noGrouping = GroupTemplateResolver.presets.first { $0.template.isEmpty } XCTAssertNotNil(noGrouping) XCTAssertEqual(noGrouping?.name, "No Grouping") } func testPlaceholdersExist() { XCTAssertEqual(GroupTemplateResolver.placeholders.count, 8) } func testAllPlaceholderTokensHaveBraces() { for placeholder in GroupTemplateResolver.placeholders { XCTAssertTrue(placeholder.token.hasPrefix("{"), "\(placeholder.token) should start with {") XCTAssertTrue(placeholder.token.hasSuffix("}"), "\(placeholder.token) should end with }") } } func testAllPlaceholderDescriptionsNonEmpty() { for placeholder in GroupTemplateResolver.placeholders { XCTAssertFalse(placeholder.description.isEmpty, "\(placeholder.token) should have a description") } } }