import XCTest @testable import MixBoard // MARK: - LyricsParser Tests final class LyricsParserTests: XCTestCase { // MARK: - parseSynced: Basic LRC Format func testParseSyncedBasic() { let lrc = """ [00:12.34] First line [00:24.56] Second line [00:36.78] Third line """ let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines[0].text, "First line") XCTAssertEqual(lines[0].timestamp, 12.34, accuracy: 0.01) XCTAssertEqual(lines[1].text, "Second line") XCTAssertEqual(lines[1].timestamp, 24.56, accuracy: 0.01) XCTAssertEqual(lines[2].text, "Third line") XCTAssertEqual(lines[2].timestamp, 36.78, accuracy: 0.01) } func testParseSyncedSortedByTimestamp() { let lrc = """ [01:00.00] Late line [00:10.00] Early line [00:30.00] Middle line """ let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines[0].text, "Early line") XCTAssertEqual(lines[1].text, "Middle line") XCTAssertEqual(lines[2].text, "Late line") } func testParseSyncedWithMilliseconds() { let lrc = "[02:05.456] Line with ms" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 125.456, accuracy: 0.001) XCTAssertEqual(lines[0].text, "Line with ms") } func testParseSyncedWithCentiseconds() { let lrc = "[01:30.50] Half second" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 90.50, accuracy: 0.01) } func testParseSyncedWithColonSeparator() { // Some LRC files use : instead of . for fractional seconds let lrc = "[01:30:50] Colon style" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 90.50, accuracy: 0.01) } func testParseSyncedWithCommaSeparator() { let lrc = "[01:30,50] Comma style" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 90.50, accuracy: 0.01) } func testParseSyncedMultipleTimestampsPerLine() { let lrc = "[00:12.00][01:24.00] Repeated chorus line" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 2) XCTAssertEqual(lines[0].timestamp, 12.0, accuracy: 0.01) XCTAssertEqual(lines[0].text, "Repeated chorus line") XCTAssertEqual(lines[1].timestamp, 84.0, accuracy: 0.01) XCTAssertEqual(lines[1].text, "Repeated chorus line") } func testParseSyncedEmptyTextLine() { let lrc = """ [00:10.00] Has text [00:20.00] [00:30.00] After break """ let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines[1].text, "") } func testParseSyncedEmptyInput() { let lines = LyricsParser.parseSynced("") XCTAssertTrue(lines.isEmpty) } func testParseSyncedNoTimestamps() { let lrc = """ Just plain text No timestamps here """ let lines = LyricsParser.parseSynced(lrc) XCTAssertTrue(lines.isEmpty) } func testParseSyncedThreeDigitMinutes() { let lrc = "[100:00.00] Very long track" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 6000.0, accuracy: 0.01) } func testParseSyncedSkipsBlankLines() { let lrc = """ [00:10.00] Line one [00:20.00] Line two """ let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 2) } func testParseSyncedZeroTimestamp() { let lrc = "[00:00.00] Very start" let lines = LyricsParser.parseSynced(lrc) XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].timestamp, 0.0, accuracy: 0.001) } // MARK: - parsePlain func testParsePlainBasic() { let text = """ First line Second line Third line """ let lines = LyricsParser.parsePlain(text) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines[0].text, "First line") XCTAssertEqual(lines[1].text, "Second line") XCTAssertEqual(lines[2].text, "Third line") } func testParsePlainTimestampsAreLineIndices() { let text = "Line A\nLine B\nLine C" let lines = LyricsParser.parsePlain(text) XCTAssertEqual(lines[0].timestamp, 0) XCTAssertEqual(lines[1].timestamp, 1) XCTAssertEqual(lines[2].timestamp, 2) } func testParsePlainEmptyInput() { let lines = LyricsParser.parsePlain("") XCTAssertEqual(lines.count, 1) XCTAssertEqual(lines[0].text, "") } func testParsePlainWithBlankLines() { let text = "Verse 1\n\nVerse 2" let lines = LyricsParser.parsePlain(text) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines[1].text, "") } // MARK: - currentLineIndex func testCurrentLineIndexBasic() { let lines = [ LyricsParser.LyricLine(timestamp: 10, text: "A"), LyricsParser.LyricLine(timestamp: 20, text: "B"), LyricsParser.LyricLine(timestamp: 30, text: "C"), ] XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 15), 0) XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 25), 1) XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 35), 2) } func testCurrentLineIndexExactMatch() { let lines = [ LyricsParser.LyricLine(timestamp: 10, text: "A"), LyricsParser.LyricLine(timestamp: 20, text: "B"), ] XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 10), 0) XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 20), 1) } func testCurrentLineIndexBeforeFirstLine() { let lines = [ LyricsParser.LyricLine(timestamp: 10, text: "A"), ] XCTAssertNil(LyricsParser.currentLineIndex(in: lines, at: 5)) } func testCurrentLineIndexEmpty() { XCTAssertNil(LyricsParser.currentLineIndex(in: [], at: 10)) } func testCurrentLineIndexZeroTime() { let lines = [ LyricsParser.LyricLine(timestamp: 0, text: "Start"), LyricsParser.LyricLine(timestamp: 10, text: "Next"), ] XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 0), 0) } func testCurrentLineIndexAfterLastLine() { let lines = [ LyricsParser.LyricLine(timestamp: 10, text: "A"), LyricsParser.LyricLine(timestamp: 20, text: "B"), ] XCTAssertEqual(LyricsParser.currentLineIndex(in: lines, at: 9999), 1) } // MARK: - LyricLine.formattedTime func testFormattedTimeZero() { let line = LyricsParser.LyricLine(timestamp: 0, text: "") XCTAssertEqual(line.formattedTime, "0:00") } func testFormattedTimeMinutesAndSeconds() { let line = LyricsParser.LyricLine(timestamp: 125.456, text: "") XCTAssertEqual(line.formattedTime, "2:05") } func testFormattedTimeLargeValue() { let line = LyricsParser.LyricLine(timestamp: 3661, text: "") XCTAssertEqual(line.formattedTime, "61:01") } // MARK: - LyricLine.Equatable func testLyricLineEquatable() { let a = LyricsParser.LyricLine(timestamp: 10, text: "Hello") let b = LyricsParser.LyricLine(timestamp: 10, text: "Hello") // Different UUIDs, so they're not equal XCTAssertNotEqual(a, b) // Same instance should be equal XCTAssertEqual(a, a) } } // MARK: - LyricsResult Tests final class LyricsResultTests: XCTestCase { func testHasSyncedLyricsTrue() { let result = LyricsResult(syncedLyrics: "[00:10.00] Hello") XCTAssertTrue(result.hasSyncedLyrics) } func testHasSyncedLyricsFalseNil() { let result = LyricsResult(syncedLyrics: nil) XCTAssertFalse(result.hasSyncedLyrics) } func testHasSyncedLyricsFalseEmpty() { let result = LyricsResult(syncedLyrics: "") XCTAssertFalse(result.hasSyncedLyrics) } func testHasLyricsWithSynced() { let result = LyricsResult(syncedLyrics: "[00:10.00] Hello") XCTAssertTrue(result.hasLyrics) } func testHasLyricsWithPlain() { let result = LyricsResult(plainLyrics: "Hello world") XCTAssertTrue(result.hasLyrics) } func testHasLyricsFalse() { let result = LyricsResult() XCTAssertFalse(result.hasLyrics) } func testIsInstrumental() { let result = LyricsResult(isInstrumental: true) XCTAssertTrue(result.isInstrumental) } func testDefaultValues() { let result = LyricsResult() XCTAssertEqual(result.trackName, "") XCTAssertEqual(result.artistName, "") XCTAssertEqual(result.albumName, "") XCTAssertFalse(result.isInstrumental) XCTAssertNil(result.plainLyrics) XCTAssertNil(result.syncedLyrics) } } // MARK: - LyricsError Tests final class LyricsErrorTests: XCTestCase { func testInvalidURLDescription() { let error = LyricsError.invalidURL XCTAssertEqual(error.errorDescription, "Invalid lyrics search URL") } func testNotFoundDescription() { let error = LyricsError.notFound XCTAssertEqual(error.errorDescription, "No lyrics found") } func testNetworkErrorDescription() { let error = LyricsError.networkError XCTAssertEqual(error.errorDescription, "Network error fetching lyrics") } func testHTTPErrorDescription() { let error = LyricsError.httpError(404) XCTAssertEqual(error.errorDescription, "HTTP error 404") } func testHTTPErrorDescription500() { let error = LyricsError.httpError(500) XCTAssertEqual(error.errorDescription, "HTTP error 500") } } // MARK: - LRCLIBService Tests final class LRCLIBServiceTests: XCTestCase { func testCacheIsInitiallyEmpty() async { let service = LRCLIBService() await service.clearCache() // No crash } func testFetchLyricsEmptyArtist() async { let service = LRCLIBService() do { _ = try await service.fetchLyrics(artist: "", title: "") // May succeed or throw — depends on network } catch { // Expected — either notFound or networkError XCTAssertTrue(error is LyricsError) } } func testFetchLyricsWellKnownSong() async throws { // Use a well-known song that's very likely in LRCLIB let service = LRCLIBService() do { let result = try await service.fetchLyrics( artist: "Queen", title: "Bohemian Rhapsody" ) XCTAssertTrue(result.hasLyrics) } catch { // Network may be unavailable in CI — don't fail print("Network test skipped: \(error)") } } func testFetchLyricsNonExistentSong() async { let service = LRCLIBService() do { _ = try await service.fetchLyrics( artist: "zzz_nonexistent_artist_12345", title: "zzz_nonexistent_title_12345" ) XCTFail("Expected LyricsError.notFound") } catch LyricsError.notFound { // Expected } catch { // Network error is also acceptable } } func testCachingWorks() async throws { let service = LRCLIBService() do { let result1 = try await service.fetchLyrics( artist: "Queen", title: "Bohemian Rhapsody" ) // Second call should hit cache (much faster) let result2 = try await service.fetchLyrics( artist: "Queen", title: "Bohemian Rhapsody" ) XCTAssertEqual(result1.trackName, result2.trackName) } catch { // Network may be unavailable — don't fail print("Network test skipped: \(error)") } } func testClearCache() async { let service = LRCLIBService() await service.clearCache() // Should not crash and cache should be empty } }