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
    }
}
