import SwiftData
import XCTest
@testable import MixBoard

// MARK: - Test Helpers

/// Creates an in-memory ModelContainer for testing SwiftData operations.
func makeInMemoryContainer() throws -> ModelContainer {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    return try ModelContainer(
        for: Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self,
        configurations: config
    )
}

// MARK: - PlaylistViewModelTests

@MainActor
final class PlaylistViewModelTests: XCTestCase {

    private var container: ModelContainer!
    private var context: ModelContext!
    private var vm: PlaylistViewModel!

    override func setUp() async throws {
        container = try makeInMemoryContainer()
        context = ModelContext(container)
        vm = PlaylistViewModel()
    }

    override func tearDown() {
        // Clean up UserDefaults keys used by mix targets
        for slot in 0..<3 {
            UserDefaults.standard.removeObject(forKey: "mixTarget\(slot)ID")
        }
    }

    // MARK: - Initial State

    func testInitialState() {
        XCTAssertNil(vm.selectedPlaylist)
        XCTAssertFalse(vm.showExportSheet)
        XCTAssertNil(vm.exportError)
        XCTAssertNil(vm.statusMessage)
        XCTAssertEqual(vm.defaultCrossfadeDuration, 2.0)
    }

    func testInitialMixTargetsNil() {
        XCTAssertEqual(vm.mixTargets.count, 3)
        XCTAssertNil(vm.mixTargets[0])
        XCTAssertNil(vm.mixTargets[1])
        XCTAssertNil(vm.mixTargets[2])
    }

    // MARK: - Mix Targets

    func testSetMixTargetValidSlot() {
        let playlist = Playlist(name: "Mix A")
        context.insert(playlist)
        try? context.save()

        vm.setMixTarget(0, playlist: playlist)
        XCTAssertEqual(vm.mixTargets[0]?.name, "Mix A")
        XCTAssertEqual(UserDefaults.standard.string(forKey: "mixTarget0ID"), playlist.id.uuidString)
    }

    func testSetMixTargetAllSlots() {
        let p0 = Playlist(name: "Red Mix")
        let p1 = Playlist(name: "Blue Mix")
        let p2 = Playlist(name: "Gold Mix")
        context.insert(p0); context.insert(p1); context.insert(p2)
        try? context.save()

        vm.setMixTarget(0, playlist: p0)
        vm.setMixTarget(1, playlist: p1)
        vm.setMixTarget(2, playlist: p2)

        XCTAssertEqual(vm.mixTargets[0]?.name, "Red Mix")
        XCTAssertEqual(vm.mixTargets[1]?.name, "Blue Mix")
        XCTAssertEqual(vm.mixTargets[2]?.name, "Gold Mix")
    }

    func testSetMixTargetInvalidSlot() {
        let playlist = Playlist(name: "Test")
        vm.setMixTarget(-1, playlist: playlist)
        vm.setMixTarget(3, playlist: playlist)
        // Should not crash, and mixTargets should remain unchanged
        XCTAssertNil(vm.mixTargets[0])
        XCTAssertNil(vm.mixTargets[1])
        XCTAssertNil(vm.mixTargets[2])
    }

    func testSetMixTargetNilClearsUserDefaults() {
        let playlist = Playlist(name: "Target")
        context.insert(playlist)
        try? context.save()

        vm.setMixTarget(0, playlist: playlist)
        XCTAssertNotNil(UserDefaults.standard.string(forKey: "mixTarget0ID"))

        vm.setMixTarget(0, playlist: nil)
        XCTAssertNil(vm.mixTargets[0])
        XCTAssertNil(UserDefaults.standard.string(forKey: "mixTarget0ID"))
    }

    func testMixTargetNameWithPlaylist() {
        let playlist = Playlist(name: "Friday Night")
        vm.setMixTarget(1, playlist: playlist)
        XCTAssertEqual(vm.mixTargetName(1), "Friday Night")
    }

    func testMixTargetNameWithoutPlaylist() {
        XCTAssertEqual(vm.mixTargetName(0), "Mix 1")
        XCTAssertEqual(vm.mixTargetName(1), "Mix 2")
        XCTAssertEqual(vm.mixTargetName(2), "Mix 3")
    }

    func testMixTargetNameInvalidSlot() {
        XCTAssertEqual(vm.mixTargetName(5), "Mix 6")
    }

    func testRestoreTargetPlaylist() {
        let playlist = Playlist(name: "Saved Target")
        context.insert(playlist)
        try? context.save()

        // Simulate persisted state
        UserDefaults.standard.set(playlist.id.uuidString, forKey: "mixTarget0ID")

        vm.restoreTargetPlaylist(from: [playlist])
        XCTAssertEqual(vm.mixTargets[0]?.name, "Saved Target")
    }

    func testRestoreTargetPlaylistNoMatch() {
        UserDefaults.standard.set(UUID().uuidString, forKey: "mixTarget0ID")
        vm.restoreTargetPlaylist(from: [])
        XCTAssertNil(vm.mixTargets[0])
    }

    // MARK: - Legacy targetPlaylist

    func testLegacyTargetPlaylist() {
        let playlist = Playlist(name: "Legacy")
        vm.targetPlaylist = playlist
        XCTAssertEqual(vm.mixTargets[0]?.name, "Legacy")
        XCTAssertEqual(vm.targetPlaylist?.name, "Legacy")
    }

    // MARK: - Playlist CRUD

    func testCreatePlaylist() {
        let playlist = vm.createPlaylist(name: "New Mix", context: context)
        XCTAssertEqual(playlist.name, "New Mix")

        let descriptor = FetchDescriptor<Playlist>()
        let fetched = try? context.fetch(descriptor)
        XCTAssertEqual(fetched?.count, 1)
        XCTAssertEqual(fetched?.first?.name, "New Mix")
    }

    func testDeletePlaylist() {
        let playlist = vm.createPlaylist(name: "To Delete", context: context)
        vm.selectedPlaylist = playlist

        vm.deletePlaylist(playlist, context: context)

        let descriptor = FetchDescriptor<Playlist>()
        let fetched = try? context.fetch(descriptor)
        XCTAssertEqual(fetched?.count, 0)
        XCTAssertNil(vm.selectedPlaylist)
    }

    func testDeletePlaylistDoesNotClearOtherSelection() {
        let p1 = vm.createPlaylist(name: "Keep", context: context)
        let p2 = vm.createPlaylist(name: "Delete", context: context)
        vm.selectedPlaylist = p1

        vm.deletePlaylist(p2, context: context)
        XCTAssertEqual(vm.selectedPlaylist?.name, "Keep")
    }

    func testRenamePlaylist() {
        let playlist = vm.createPlaylist(name: "Original", context: context)
        let originalDate = playlist.dateModified

        vm.renamePlaylist(playlist, to: "Renamed", context: context)
        XCTAssertEqual(playlist.name, "Renamed")
        XCTAssertGreaterThanOrEqual(playlist.dateModified, originalDate)
    }

    // MARK: - Track Management

    func testAddTrackToPlaylist() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        vm.addTrack(track, to: playlist, context: context)
        XCTAssertEqual(playlist.trackCount, 1)
        XCTAssertEqual(playlist.sortedEntries.first?.crossfadeDuration, 2.0) // default
    }

    func testAddDuplicateTrackShowsWarning() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        vm.addTrack(track, to: playlist, context: context)
        vm.addTrack(track, to: playlist, context: context)

        // Second add should be rejected (duplicate)
        XCTAssertEqual(playlist.trackCount, 1)
        XCTAssertNotNil(vm.statusMessage)
    }

    func testAddTracksMultiple() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let t1 = Track(title: "A", filePath: "Music/b.mp3") // intentionally out of alphabetical order
        let t2 = Track(title: "B", filePath: "Music/a.mp3")
        context.insert(t1); context.insert(t2)

        vm.addTracks([t1, t2], to: playlist, context: context)
        XCTAssertEqual(playlist.trackCount, 2)
        // Should be sorted by filePath
        let sorted = playlist.sortedEntries
        XCTAssertEqual(sorted[0].track?.filePath, "Music/a.mp3")
        XCTAssertEqual(sorted[1].track?.filePath, "Music/b.mp3")
    }

    func testAddTracksSkipsDuplicates() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        vm.addTrack(track, to: playlist, context: context)
        vm.addTracks([track], to: playlist, context: context)

        XCTAssertEqual(playlist.trackCount, 1)
    }

    func testRemoveEntry() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.addTrack(track, to: playlist, context: context)

        let entry = playlist.sortedEntries.first!
        vm.removeEntry(entry, from: playlist, context: context)
        XCTAssertEqual(playlist.trackCount, 0)
    }

    func testMoveEntry() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        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")
        context.insert(t1); context.insert(t2); context.insert(t3)
        playlist.addTrack(t1); playlist.addTrack(t2); playlist.addTrack(t3)

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

    // MARK: - Quick Add

    func testQuickAddToMixNoTarget() {
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        let result = vm.quickAddToMix(slot: 0, track: track, context: context)
        XCTAssertFalse(result)
        XCTAssertNotNil(vm.statusMessage)
    }

    func testQuickAddToMixSuccess() {
        let playlist = vm.createPlaylist(name: "Target", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.setMixTarget(0, playlist: playlist)

        let result = vm.quickAddToMix(slot: 0, track: track, context: context)
        XCTAssertTrue(result)
        XCTAssertEqual(playlist.trackCount, 1)
    }

    func testQuickAddToTargetLegacy() {
        let playlist = vm.createPlaylist(name: "Target", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.targetPlaylist = playlist

        let result = vm.quickAddToTarget(track: track, context: context)
        XCTAssertTrue(result)
        XCTAssertEqual(playlist.trackCount, 1)
    }

    func testQuickAddDuplicate() {
        let playlist = vm.createPlaylist(name: "Target", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.setMixTarget(0, playlist: playlist)

        _ = vm.quickAddToMix(slot: 0, track: track, context: context)
        let result2 = vm.quickAddToMix(slot: 0, track: track, context: context)
        XCTAssertFalse(result2)
        XCTAssertEqual(playlist.trackCount, 1)
    }

    // MARK: - Crossfade & Gain

    func testUpdateCrossfade() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.addTrack(track, to: playlist, context: context)

        let entry = playlist.sortedEntries.first!
        vm.updateCrossfade(for: entry, duration: 5.0, context: context)
        XCTAssertEqual(entry.crossfadeDuration, 5.0)
    }

    func testUpdateGain() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)
        vm.addTrack(track, to: playlist, context: context)

        let entry = playlist.sortedEntries.first!
        vm.updateGain(for: entry, gain: 3.5, context: context)
        XCTAssertEqual(entry.gainAdjustment, 3.5)
    }

    // MARK: - Cue Points

    func testAddCuePoint() {
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        vm.addCuePoint(to: track, at: 30.0, type: .drop, name: "The Drop", context: context)
        XCTAssertEqual(track.cuePoints.count, 1)
        XCTAssertEqual(track.cuePoints.first?.name, "The Drop")
        XCTAssertEqual(track.cuePoints.first?.type, .drop)
        XCTAssertEqual(track.cuePoints.first?.timestamp, 30.0)
    }

    func testRemoveCuePoint() {
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        vm.addCuePoint(to: track, at: 10.0, name: "Marker", context: context)
        XCTAssertEqual(track.cuePoints.count, 1)

        let cuePoint = track.cuePoints.first!
        vm.removeCuePoint(cuePoint, from: track, context: context)
        XCTAssertEqual(track.cuePoints.count, 0)
    }

    // MARK: - Mix Duration

    func testMixDurationEmpty() {
        let playlist = Playlist(name: "Empty")
        XCTAssertEqual(vm.mixDuration(for: playlist), 0)
    }

    func testMixDurationSingleTrack() {
        let playlist = Playlist(name: "Mix")
        let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 200)
        playlist.addTrack(track)
        XCTAssertEqual(vm.mixDuration(for: playlist), 200, accuracy: 0.01)
    }

    func testMixDurationWithCrossfades() {
        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)
        let t3 = Track(title: "C", filePath: "Music/c.mp3", duration: 240)
        playlist.addTrack(t1, crossfadeDuration: 0)
        playlist.addTrack(t2, crossfadeDuration: 5)
        playlist.addTrack(t3, crossfadeDuration: 3)

        // total = 200 + 180 + 240 - 5 - 3 = 612
        XCTAssertEqual(vm.mixDuration(for: playlist), 612, accuracy: 0.01)
    }

    // MARK: - Show Status

    func testShowStatus() {
        vm.showStatus("Test message")
        XCTAssertEqual(vm.statusMessage, "Test message")
    }

    func testShowStatusNilInitially() {
        XCTAssertNil(vm.statusMessage)
    }

    // MARK: - isDuplicate

    func testIsDuplicateWithContext() {
        let playlist = vm.createPlaylist(name: "Mix", context: context)
        let track = Track(title: "Song", filePath: "Music/song.mp3")
        context.insert(track)

        XCTAssertFalse(vm.isDuplicate(track: track, in: playlist, context: context))

        vm.addTrack(track, to: playlist, context: context)
        XCTAssertTrue(vm.isDuplicate(track: track, in: playlist, context: context))
    }

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

        XCTAssertFalse(vm.isDuplicate(track: track, in: playlist))
        playlist.addTrack(track)
        XCTAssertTrue(vm.isDuplicate(track: track, in: playlist))
    }
}
