import XCTest
import SwiftUI
@testable import MixBoard

// MARK: - Model Tests

final class ModelTests: XCTestCase {

    func testTrackInit() {
        let track = Track(
            title: "Test Song",
            artist: "Test Artist",
            filePath: "Music/test.mp3",
            fileName: "test.mp3",
            duration: 180
        )
        XCTAssertEqual(track.title, "Test Song")
        XCTAssertEqual(track.artist, "Test Artist")
        XCTAssertEqual(track.fileName, "test.mp3")
        XCTAssertEqual(track.formattedDuration, "3:00")
        XCTAssertFalse(track.isAnalyzed)
        XCTAssertNil(track.bpm)
    }

    func testTrackDefaults() {
        let track = Track(title: "Minimal", filePath: "Music/minimal.wav")
        XCTAssertEqual(track.artist, "")
        XCTAssertEqual(track.album, "")
        XCTAssertEqual(track.genre, "")
        XCTAssertEqual(track.playCount, 0)
        XCTAssertEqual(track.rating, 0)
        XCTAssertNil(track.lastPlayed)
        XCTAssertNil(track.color)
        XCTAssertNil(track.waveformData)
        XCTAssertEqual(track.notes, "")
        XCTAssertTrue(track.cuePoints.isEmpty)
    }

    func testTrackFormattedBPM() {
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        XCTAssertEqual(track.formattedBPM, "—")
        track.bpm = 128.5
        XCTAssertEqual(track.formattedBPM, "128.5")
    }

    func testTrackFormattedDurationEdgeCases() {
        let track0 = Track(title: "Zero", filePath: "Music/z.mp3", duration: 0)
        XCTAssertEqual(track0.formattedDuration, "0:00")

        let trackLong = Track(title: "Long", filePath: "Music/l.mp3", duration: 3661)
        XCTAssertEqual(trackLong.formattedDuration, "61:01")
    }

    func testFileNameExtraction() {
        let track = Track(title: "Song", filePath: "Music/subfolder/Artist - Song.flac")
        XCTAssertEqual(track.fileName, "Artist - Song.flac")
    }

    func testFileNameExplicit() {
        let track = Track(title: "Song", filePath: "Music/song.mp3", fileName: "custom.mp3")
        XCTAssertEqual(track.fileName, "custom.mp3")
    }

    func testTrackFileURL() {
        let track = Track(title: "Song", filePath: "Music/test.mp3")
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        XCTAssertEqual(track.fileURL, docs.appendingPathComponent("Music/test.mp3"))
    }
}

// MARK: - Playlist Tests

final class PlaylistTests: XCTestCase {

    func testPlaylistInit() {
        let playlist = Playlist(name: "Friday Mix")
        XCTAssertEqual(playlist.name, "Friday Mix")
        XCTAssertEqual(playlist.color, "#2196F3")
        XCTAssertEqual(playlist.trackCount, 0)
        XCTAssertNil(playlist.targetBPM)
        XCTAssertTrue(playlist.entries.isEmpty)
        XCTAssertNil(playlist.folder)
    }

    func testPlaylistAddTrack() {
        let playlist = Playlist(name: "My Mix")
        let track = Track(title: "Song", filePath: "Music/song.mp3")

        XCTAssertEqual(playlist.trackCount, 0)
        playlist.addTrack(track, crossfadeDuration: 2.0)
        XCTAssertEqual(playlist.trackCount, 1)

        let entry = playlist.sortedEntries.first
        XCTAssertEqual(entry?.position, 0)
        XCTAssertEqual(entry?.crossfadeDuration, 2.0)
    }

    func testPlaylistAddMultipleTracks() {
        let playlist = Playlist(name: "Mix")
        let t1 = Track(title: "First", filePath: "Music/first.mp3", duration: 200)
        let t2 = Track(title: "Second", filePath: "Music/second.mp3", duration: 180)
        let t3 = Track(title: "Third", filePath: "Music/third.mp3", duration: 240)

        playlist.addTrack(t1)
        playlist.addTrack(t2)
        playlist.addTrack(t3)

        XCTAssertEqual(playlist.trackCount, 3)
        let sorted = playlist.sortedEntries
        XCTAssertEqual(sorted[0].track?.title, "First")
        XCTAssertEqual(sorted[1].track?.title, "Second")
        XCTAssertEqual(sorted[2].track?.title, "Third")
        XCTAssertEqual(sorted[0].position, 0)
        XCTAssertEqual(sorted[1].position, 1)
        XCTAssertEqual(sorted[2].position, 2)
    }

    func testPlaylistTotalDuration() {
        let playlist = Playlist(name: "Mix")
        let t1 = Track(title: "A", filePath: "Music/a.mp3", duration: 200)
        let t2 = Track(title: "B", filePath: "Music/b.mp3", duration: 180)
        playlist.addTrack(t1)
        playlist.addTrack(t2)
        XCTAssertEqual(playlist.totalDuration, 380, accuracy: 0.01)
    }

    func testPlaylistFormattedTotalDuration() {
        let playlist = Playlist(name: "Mix")
        playlist.addTrack(Track(title: "A", filePath: "Music/a.mp3", duration: 3700))
        XCTAssertTrue(playlist.formattedTotalDuration.contains(":"))
    }

    func testPlaylistRemoveEntry() {
        let playlist = Playlist(name: "Mix")
        let t1 = Track(title: "A", filePath: "Music/a.mp3")
        let t2 = Track(title: "B", filePath: "Music/b.mp3")
        let t3 = Track(title: "C", filePath: "Music/c.mp3")
        playlist.addTrack(t1)
        playlist.addTrack(t2)
        playlist.addTrack(t3)

        playlist.removeEntry(at: 1)  // Remove "B"
        XCTAssertEqual(playlist.trackCount, 2)

        let sorted = playlist.sortedEntries
        XCTAssertEqual(sorted[0].track?.title, "A")
        XCTAssertEqual(sorted[1].track?.title, "C")
        // Positions should be re-indexed
        XCTAssertEqual(sorted[0].position, 0)
        XCTAssertEqual(sorted[1].position, 1)
    }

    func testPlaylistMoveEntry() {
        let playlist = Playlist(name: "Mix")
        let t1 = Track(title: "A", filePath: "Music/a.mp3")
        let t2 = Track(title: "B", filePath: "Music/b.mp3")
        let t3 = Track(title: "C", filePath: "Music/c.mp3")
        playlist.addTrack(t1)
        playlist.addTrack(t2)
        playlist.addTrack(t3)

        // Move C from position 2 to position 0
        playlist.moveEntry(from: 2, to: 0)
        let sorted = playlist.sortedEntries
        XCTAssertEqual(sorted[0].track?.title, "C")
        XCTAssertEqual(sorted[1].track?.title, "A")
        XCTAssertEqual(sorted[2].track?.title, "B")
    }

    func testPlaylistDateModifiedUpdates() {
        let playlist = Playlist(name: "Mix")
        let originalDate = playlist.dateModified

        // Small delay to ensure date changes
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        playlist.addTrack(track)
        XCTAssertGreaterThanOrEqual(playlist.dateModified, originalDate)
    }
}

// MARK: - PlaylistEntry Tests

final class PlaylistEntryTests: XCTestCase {

    func testEntryEffectiveDuration() {
        let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 300)
        let entry = PlaylistEntry(position: 0, track: track)
        XCTAssertEqual(entry.effectiveDuration, 300, accuracy: 0.01)
    }

    func testEntryEffectiveDurationWithOffsets() {
        let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 300)
        let entry = PlaylistEntry(position: 0, track: track, startOffset: 10, endOffset: 280)
        XCTAssertEqual(entry.effectiveDuration, 270, accuracy: 0.01)
    }

    func testEntryEffectiveDurationNoTrack() {
        let entry = PlaylistEntry(position: 0, track: nil)
        XCTAssertEqual(entry.effectiveDuration, 0)
    }

    func testEntryDefaults() {
        let entry = PlaylistEntry(position: 5, track: nil)
        XCTAssertEqual(entry.position, 5)
        XCTAssertEqual(entry.crossfadeDuration, 0)
        XCTAssertEqual(entry.startOffset, 0)
        XCTAssertEqual(entry.endOffset, 0)
        XCTAssertEqual(entry.gainAdjustment, 0)
        XCTAssertEqual(entry.notes, "")
    }
}

// MARK: - CuePoint Tests

final class CuePointTests: XCTestCase {

    func testCuePointFormatTime() {
        XCTAssertEqual(CuePoint.formatTime(0), "00:00.000")
        XCTAssertEqual(CuePoint.formatTime(125.456), "02:05.456")
        XCTAssertEqual(CuePoint.formatTime(59.999), "00:59.999")
    }

    func testCuePointInit() {
        let cue = CuePoint(name: "Drop", timestamp: 45.5, type: .drop)
        XCTAssertEqual(cue.name, "Drop")
        XCTAssertEqual(cue.timestamp, 45.5)
        XCTAssertEqual(cue.type, .drop)
        XCTAssertFalse(cue.isRegion)
    }

    func testCuePointRegion() {
        let cue = CuePoint(name: "Verse", timestamp: 30, endTimestamp: 90, type: .verse)
        XCTAssertTrue(cue.isRegion)
        XCTAssertEqual(cue.endTimestamp, 90)
    }

    func testCuePointComparable() {
        let a = CuePoint(timestamp: 10)
        let b = CuePoint(timestamp: 20)
        let c = CuePoint(timestamp: 5)
        XCTAssertTrue(c < a)
        XCTAssertTrue(a < b)
        let sorted = [b, a, c].sorted()
        XCTAssertEqual(sorted.map(\.timestamp), [5, 10, 20])
    }

    func testCuePointTypes() {
        XCTAssertEqual(CuePointType.allCases.count, 11)
        XCTAssertEqual(CuePointType.marker.rawValue, "Marker")
        XCTAssertEqual(CuePointType.fadeOut.rawValue, "Fade Out")
    }
}

// MARK: - PlaylistFolder Tests

final class PlaylistFolderTests: XCTestCase {

    func testFolderInit() {
        let folder = PlaylistFolder(name: "My Mixes")
        XCTAssertEqual(folder.name, "My Mixes")
        XCTAssertTrue(folder.isExpanded)
        XCTAssertTrue(folder.playlists.isEmpty)
        XCTAssertEqual(folder.totalTrackCount, 0)
    }
}

// MARK: - AppState Tests

final class AppStateTests: XCTestCase {

    override func tearDown() {
        // Clean up UserDefaults
        UserDefaults.standard.removeObject(forKey: "appState.lastPlaylistID")
        UserDefaults.standard.removeObject(forKey: "appState.lastEntryID")
        UserDefaults.standard.removeObject(forKey: "appState.lastTrackFilePath")
        UserDefaults.standard.removeObject(forKey: "appState.lastPlaybackTime")
    }

    func testSaveAndLoadPlaylistID() {
        let id = UUID()
        AppState.saveLastPlaylist(id: id)
        XCTAssertEqual(AppState.lastPlaylistID, id)
    }

    func testSaveAndLoadEntryID() {
        let id = UUID()
        AppState.saveLastEntry(id: id)
        XCTAssertEqual(AppState.lastEntryID, id)
    }

    func testSaveAndLoadTrackFilePath() {
        AppState.saveLastTrack(filePath: "Music/test.mp3")
        XCTAssertEqual(AppState.lastTrackFilePath, "Music/test.mp3")
    }

    func testSaveAndLoadPlaybackTime() {
        AppState.savePlaybackTime(42.5)
        XCTAssertEqual(AppState.lastPlaybackTime, 42.5, accuracy: 0.01)
    }

    func testSavePlaybackStateAll() {
        let playlistID = UUID()
        let entryID = UUID()
        AppState.savePlaybackState(
            playlistID: playlistID,
            entryID: entryID,
            trackFilePath: "Music/combined.flac",
            playbackTime: 99.9
        )
        XCTAssertEqual(AppState.lastPlaylistID, playlistID)
        XCTAssertEqual(AppState.lastEntryID, entryID)
        XCTAssertEqual(AppState.lastTrackFilePath, "Music/combined.flac")
        XCTAssertEqual(AppState.lastPlaybackTime, 99.9, accuracy: 0.01)
    }

    func testDefaultsAreNil() {
        XCTAssertNil(AppState.lastPlaylistID)
        XCTAssertNil(AppState.lastEntryID)
    }
}

// MARK: - Sync Encoding/Decoding Tests

final class SyncTests: XCTestCase {

    func testSyncPayloadRoundTrip() throws {
        let playlist = Playlist(name: "Test Mix", notes: "Great tracks", color: "#FF5722")
        playlist.targetBPM = 128.0
        let t1 = Track(title: "Song A", artist: "Artist A", filePath: "Music/a.mp3", fileName: "a.mp3", duration: 200)
        t1.bpm = 126
        t1.musicalKey = "Am"
        let t2 = Track(title: "Song B", artist: "Artist B", filePath: "Music/b.flac", fileName: "b.flac", duration: 180)
        playlist.addTrack(t1, crossfadeDuration: 3.0)
        playlist.addTrack(t2, crossfadeDuration: 2.0)

        let syncPlaylist = SyncPlaylist(from: playlist)
        let payload = SyncPayload(version: 1, exportedAt: Date(), exportedFrom: "Test", playlists: [syncPlaylist])

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        encoder.outputFormatting = .prettyPrinted
        let data = try encoder.encode(payload)

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let decoded = try decoder.decode(SyncPayload.self, from: data)

        XCTAssertEqual(decoded.version, 1)
        XCTAssertEqual(decoded.playlists.count, 1)

        let dp = decoded.playlists[0]
        XCTAssertEqual(dp.name, "Test Mix")
        XCTAssertEqual(dp.notes, "Great tracks")
        XCTAssertEqual(dp.color, "#FF5722")
        XCTAssertEqual(dp.targetBPM, 128.0)
        XCTAssertEqual(dp.entries.count, 2)

        XCTAssertEqual(dp.entries[0].filename, "a.mp3")
        XCTAssertEqual(dp.entries[0].title, "Song A")
        XCTAssertEqual(dp.entries[0].artist, "Artist A")
        XCTAssertEqual(dp.entries[0].bpm, 126)
        XCTAssertEqual(dp.entries[0].musicalKey, "Am")
        XCTAssertEqual(dp.entries[0].crossfadeDuration, 3.0)

        XCTAssertEqual(dp.entries[1].filename, "b.flac")
        XCTAssertEqual(dp.entries[1].title, "Song B")
    }

    func testSyncEmptyPlaylist() throws {
        let playlist = Playlist(name: "Empty")
        let sp = SyncPlaylist(from: playlist)
        XCTAssertTrue(sp.entries.isEmpty)

        let payload = SyncPayload(version: 1, exportedAt: Date(), exportedFrom: "iPhone", playlists: [sp])
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let data = try encoder.encode(payload)
        let decoded = try JSONDecoder().apply { $0.dateDecodingStrategy = .iso8601 }.decode(SyncPayload.self, from: data)
        XCTAssertEqual(decoded.playlists[0].entries.count, 0)
    }

    func testSyncEntryWithNoTrack() {
        let entry = PlaylistEntry(position: 0, track: nil, notes: "Missing track")
        let syncEntry = SyncEntry(from: entry)
        XCTAssertEqual(syncEntry.filename, "unknown")
        XCTAssertEqual(syncEntry.title, "Unknown")
        XCTAssertEqual(syncEntry.notes, "Missing track")
    }

    func testSyncMultiplePlaylists() throws {
        let p1 = Playlist(name: "Mix 1")
        let p2 = Playlist(name: "Mix 2")
        p1.addTrack(Track(title: "Song", filePath: "Music/s.mp3"))

        let payload = SyncPayload(
            version: 1,
            exportedAt: Date(),
            exportedFrom: "iPad",
            playlists: [SyncPlaylist(from: p1), SyncPlaylist(from: p2)]
        )

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let data = try encoder.encode(payload)

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let decoded = try decoder.decode(SyncPayload.self, from: data)
        XCTAssertEqual(decoded.playlists.count, 2)
        XCTAssertEqual(decoded.exportedFrom, "iPad")
    }
}

// MARK: - MetadataService Tests

final class MetadataServiceTests: XCTestCase {

    func testSupportedFormats() {
        let supported = ["mp3", "wav", "aif", "aiff", "flac", "m4a", "aac", "caf", "alac", "ogg"]
        for ext in supported {
            XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.\(ext)")),
                          "\(ext) should be supported")
        }
    }

    func testUnsupportedFormats() {
        let unsupported = ["txt", "jpg", "pdf", "mp4", "wma", "doc", "zip"]
        for ext in unsupported {
            XCTAssertFalse(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.\(ext)")),
                           "\(ext) should not be supported")
        }
    }

    func testCaseInsensitive() {
        XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.MP3")))
        XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.Flac")))
        XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.WAV")))
    }
}

// MARK: - AppTheme Tests

final class AppThemeTests: XCTestCase {

    func testDefaultSkin() {
        let theme = AppTheme()
        XCTAssertNotNil(theme.currentSkin)
    }

    func testWinampSkin() {
        let theme = AppTheme()
        theme.currentSkin = .winamp
        XCTAssertTrue(theme.useDarkMode)
        XCTAssertEqual(theme.preferredColorScheme, .dark)
        XCTAssertEqual(theme.cornerRadius, 4)
    }

    func testFoobarSkin() {
        let theme = AppTheme()
        theme.currentSkin = .foobarLight
        XCTAssertFalse(theme.useDarkMode)
        XCTAssertEqual(theme.preferredColorScheme, .light)
        XCTAssertEqual(theme.cornerRadius, 6)
    }

    func testFoobarDarkSkin() {
        let theme = AppTheme()
        theme.currentSkin = .foobarDark
        XCTAssertTrue(theme.useDarkMode)
        XCTAssertEqual(theme.preferredColorScheme, .dark)
    }

    func testVinylSkin() {
        let theme = AppTheme()
        theme.currentSkin = .vinyl
        XCTAssertTrue(theme.useDarkMode)
        XCTAssertEqual(theme.preferredColorScheme, .dark)
    }

    func testObsidianSkin() {
        let theme = AppTheme()
        theme.currentSkin = .obsidian
        XCTAssertTrue(theme.useDarkMode)
        XCTAssertEqual(theme.cornerRadius, 12)
    }

    func testWmpSkin() {
        let theme = AppTheme()
        theme.currentSkin = .wmp
        XCTAssertTrue(theme.useDarkMode)
        XCTAssertEqual(theme.cornerRadius, 8)
    }

    func testSkinCount() {
        XCTAssertEqual(AppTheme.Skin.allCases.count, 7)
    }

    func testSkinPersistence() {
        let theme = AppTheme()
        theme.currentSkin = .foobarLight
        XCTAssertEqual(UserDefaults.standard.string(forKey: "appThemeSkin"), "foobar Light")
        theme.currentSkin = .winamp
        XCTAssertEqual(UserDefaults.standard.string(forKey: "appThemeSkin"), "Winamp")
    }
}

// MARK: - Color Extension Tests

final class ColorTests: XCTestCase {

    func testValidHexColors() {
        XCTAssertNotNil(Color(hex: "#FF0000"))
        XCTAssertNotNil(Color(hex: "00FF00"))
        XCTAssertNotNil(Color(hex: "#2196F3"))
    }

    func testInvalidHexColors() {
        XCTAssertNil(Color(hex: ""))
        XCTAssertNil(Color(hex: "XYZ"))
        XCTAssertNil(Color(hex: "#12345"))  // 5 chars
    }
}

// MARK: - Helper

extension JSONDecoder {
    func apply(_ configure: (JSONDecoder) -> Void) -> JSONDecoder {
        configure(self)
        return self
    }
}
