import SwiftData
import XCTest
@testable import MixBoard

@MainActor
final class SyncManagerTests: XCTestCase {

    private var container: ModelContainer!
    private var context: ModelContext!
    private var syncManager: SyncManager!

    override func setUp() async throws {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        container = try ModelContainer(
            for: Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self,
            configurations: config
        )
        context = ModelContext(container)
        syncManager = SyncManager()
    }

    override func tearDown() {
        // Clean up any exported sync file
        try? FileManager.default.removeItem(at: SyncManager.syncFileURL)
    }

    // MARK: - Sync File Paths

    func testSyncDirectoryExists() {
        let dir = SyncManager.syncDirectory
        var isDir: ObjCBool = false
        XCTAssertTrue(FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir))
        XCTAssertTrue(isDir.boolValue)
    }

    func testSyncFileURLHasCorrectFilename() {
        let url = SyncManager.syncFileURL
        XCTAssertEqual(url.lastPathComponent, "mixboard-playlists.json")
    }

    // MARK: - Export

    func testExportEmptyPlaylists() {
        syncManager.exportPlaylists([])
        XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
        XCTAssertNotNil(syncManager.lastSyncDate)
        XCTAssertFalse(syncManager.isSyncing)
    }

    func testExportSinglePlaylist() {
        let playlist = Playlist(name: "Friday Mix", notes: "Great set", color: "#FF0000")
        playlist.targetBPM = 128.0
        let track = Track(title: "Anthem", artist: "DJ Test", filePath: "Music/anthem.mp3", fileName: "anthem.mp3", duration: 240)
        track.bpm = 126.5
        track.musicalKey = "Am"
        playlist.addTrack(track, crossfadeDuration: 3.0)

        syncManager.exportPlaylists([playlist])

        XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
        XCTAssertNil(syncManager.syncError)
    }

    func testExportMultiplePlaylists() {
        let p1 = Playlist(name: "Mix 1")
        let p2 = Playlist(name: "Mix 2")
        p1.addTrack(Track(title: "Song A", filePath: "Music/a.mp3", fileName: "a.mp3"))
        p2.addTrack(Track(title: "Song B", filePath: "Music/b.mp3", fileName: "b.mp3"))

        syncManager.exportPlaylists([p1, p2])
        XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))
        XCTAssertNil(syncManager.syncError)
    }

    // MARK: - Import

    func testImportFromExportedFile() throws {
        let playlist = Playlist(name: "Round Trip")
        let track = Track(title: "Test", artist: "Artist", filePath: "Music/test.mp3", fileName: "test.mp3", duration: 180)
        playlist.addTrack(track)

        syncManager.exportPlaylists([playlist])

        let imported = try syncManager.importPlaylists(from: SyncManager.syncFileURL, context: context)
        XCTAssertEqual(imported.count, 1)
        XCTAssertEqual(imported[0].name, "Round Trip")
        XCTAssertEqual(imported[0].entries.count, 1)
        XCTAssertEqual(imported[0].entries[0].filename, "test.mp3")
    }

    func testImportInvalidJSON() {
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("invalid.json")
        try? "not json at all".data(using: .utf8)?.write(to: tempURL)
        defer { try? FileManager.default.removeItem(at: tempURL) }

        XCTAssertThrowsError(try syncManager.importPlaylists(from: tempURL, context: context))
    }

    func testImportNonExistentFile() {
        let fakeURL = URL(fileURLWithPath: "/nonexistent/fake.json")
        XCTAssertThrowsError(try syncManager.importPlaylists(from: fakeURL, context: context))
    }

    // MARK: - Merge

    func testMergeWithMatchingTracks() throws {
        // Insert a track into the DB
        let track = Track(title: "Matched", artist: "DJ", filePath: "Music/matched.mp3", fileName: "matched.mp3", duration: 200)
        context.insert(track)
        try context.save()

        // Create sync data referencing the same filename
        let syncEntry = SyncEntry(from: PlaylistEntry(position: 0, track: track))
        let syncPlaylist = SyncPlaylist(
            from: {
                let pl = Playlist(name: "Imported Mix")
                pl.addTrack(track)
                return pl
            }()
        )

        let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [track], context: context)
        XCTAssertEqual(result.created, 1)
        XCTAssertEqual(result.matched, 1)
        XCTAssertEqual(result.unmatched, 0)
    }

    func testMergeWithUnmatchedTracks() {
        // No tracks in the DB, so all entries will be unmatched
        let playlist = Playlist(name: "Unmatched")
        let fakeTrack = Track(title: "Missing", filePath: "Music/missing.mp3", fileName: "missing.mp3")
        playlist.addTrack(fakeTrack)
        let syncPlaylist = SyncPlaylist(from: playlist)

        let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [], context: context)
        XCTAssertEqual(result.created, 1)
        XCTAssertEqual(result.matched, 0)
        XCTAssertEqual(result.unmatched, 1)
    }

    func testMergeCreatesCorrectNumberOfPlaylists() {
        let p1 = Playlist(name: "Mix A")
        let p2 = Playlist(name: "Mix B")
        let p3 = Playlist(name: "Mix C")
        let syncs = [SyncPlaylist(from: p1), SyncPlaylist(from: p2), SyncPlaylist(from: p3)]

        let result = syncManager.mergeImportedPlaylists(syncs, existingTracks: [], context: context)
        XCTAssertEqual(result.created, 3)
    }

    func testMergePreservesCrossfadeSettings() throws {
        let track = Track(title: "Song", filePath: "Music/song.mp3", fileName: "song.mp3", duration: 300)
        context.insert(track)
        try context.save()

        let playlist = Playlist(name: "Mix")
        playlist.addTrack(track, crossfadeDuration: 5.0)

        // Modify the entry offsets
        let entry = playlist.sortedEntries.first!
        entry.startOffset = 10
        entry.endOffset = 280
        entry.gainAdjustment = 3.5

        let syncPlaylist = SyncPlaylist(from: playlist)
        let result = syncManager.mergeImportedPlaylists([syncPlaylist], existingTracks: [track], context: context)

        XCTAssertEqual(result.matched, 1)

        // Verify the merged playlist has the correct settings
        let descriptor = FetchDescriptor<Playlist>(predicate: #Predicate { $0.name == "Mix" })
        let fetched = try context.fetch(descriptor)
        XCTAssertEqual(fetched.count, 1)

        let mergedEntry = fetched.first?.sortedEntries.first
        XCTAssertEqual(mergedEntry?.crossfadeDuration, 5.0)
        XCTAssertEqual(mergedEntry?.startOffset, 10)
        XCTAssertEqual(mergedEntry?.endOffset, 280)
        XCTAssertEqual(mergedEntry?.gainAdjustment, 3.5)
    }

    // MARK: - Round-Trip Export → Import → Merge

    func testFullRoundTrip() throws {
        // Create and export
        let track1 = Track(title: "Alpha", artist: "DJ One", filePath: "Music/alpha.mp3", fileName: "alpha.mp3", duration: 200)
        track1.bpm = 128
        track1.musicalKey = "Cm"
        let track2 = Track(title: "Beta", artist: "DJ Two", filePath: "Music/beta.flac", fileName: "beta.flac", duration: 180)

        let playlist = Playlist(name: "Export Test", notes: "Testing sync", color: "#00FF00")
        playlist.targetBPM = 130
        playlist.addTrack(track1, crossfadeDuration: 3.0)
        playlist.addTrack(track2, crossfadeDuration: 2.5)

        syncManager.exportPlaylists([playlist])
        XCTAssertTrue(FileManager.default.fileExists(atPath: SyncManager.syncFileURL.path))

        // Import back
        let imported = try syncManager.importPlaylists(from: SyncManager.syncFileURL, context: context)
        XCTAssertEqual(imported.count, 1)

        let sp = imported[0]
        XCTAssertEqual(sp.name, "Export Test")
        XCTAssertEqual(sp.notes, "Testing sync")
        XCTAssertEqual(sp.color, "#00FF00")
        XCTAssertEqual(sp.targetBPM, 130)
        XCTAssertEqual(sp.entries.count, 2)
        XCTAssertEqual(sp.entries[0].filename, "alpha.mp3")
        XCTAssertEqual(sp.entries[0].bpm, 128)
        XCTAssertEqual(sp.entries[0].musicalKey, "Cm")
        XCTAssertEqual(sp.entries[0].crossfadeDuration, 3.0)
        XCTAssertEqual(sp.entries[1].filename, "beta.flac")
        XCTAssertEqual(sp.entries[1].crossfadeDuration, 2.5)

        // Merge with matching tracks in DB
        context.insert(track1)
        context.insert(track2)
        try context.save()

        let result = syncManager.mergeImportedPlaylists(imported, existingTracks: [track1, track2], context: context)
        XCTAssertEqual(result.created, 1)
        XCTAssertEqual(result.matched, 2)
        XCTAssertEqual(result.unmatched, 0)
    }

    // MARK: - SyncPayload Encoding

    func testSyncPayloadVersionField() throws {
        syncManager.exportPlaylists([])
        let data = try Data(contentsOf: SyncManager.syncFileURL)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let payload = try decoder.decode(SyncPayload.self, from: data)
        XCTAssertEqual(payload.version, 1)
    }

    func testSyncPayloadExportedFromField() throws {
        syncManager.exportPlaylists([])
        let data = try Data(contentsOf: SyncManager.syncFileURL)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let payload = try decoder.decode(SyncPayload.self, from: data)
        XCTAssertFalse(payload.exportedFrom.isEmpty)
    }

    // MARK: - State

    func testInitialState() {
        XCTAssertNil(syncManager.lastSyncDate)
        XCTAssertFalse(syncManager.isSyncing)
        XCTAssertNil(syncManager.syncError)
    }

    func testExportUpdatesLastSyncDate() {
        XCTAssertNil(syncManager.lastSyncDate)
        syncManager.exportPlaylists([])
        XCTAssertNotNil(syncManager.lastSyncDate)
    }
}
