import Foundation
import Testing
import GRDB
@testable import Music

struct DatabaseServiceTests {
    // Creates an in-memory DatabaseService, inserts a track, and fetches it back.
    @Test func insertAndFetchTrack() throws {
        let db = try DatabaseService(inMemory: true)
        var track = Track.fixture(title: "Test Song", artist: "Test Artist")
        try db.insert(&track)

        let tracks = try db.fetchTracks(search: "", sortColumn: "title", ascending: true)
        #expect(tracks.count == 1)
        #expect(tracks[0].title == "Test Song")
    }

    // Inserts multiple tracks and verifies sorting by different columns.
    @Test func fetchTracksSortedByArtist() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Zebra", artist: "Alpha")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Alpha", artist: "Zebra")
        try db.insert(&t1)
        try db.insert(&t2)

        let ascending = try db.fetchTracks(search: "", sortColumn: "artist", ascending: true)
        #expect(ascending[0].artist == "Alpha")
        #expect(ascending[1].artist == "Zebra")

        let descending = try db.fetchTracks(search: "", sortColumn: "artist", ascending: false)
        #expect(descending[0].artist == "Zebra")
    }

    // Searches using FTS5 and verifies only matching tracks are returned.
    @Test func fts5Search() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Bohemian Rhapsody", artist: "Queen")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Hotel California", artist: "Eagles")
        var t3 = Track.fixture(fileURL: "/c.mp3", title: "Stairway to Heaven", artist: "Led Zeppelin")
        try db.insert(&t1)
        try db.insert(&t2)
        try db.insert(&t3)

        let results = try db.fetchTracks(search: "queen", sortColumn: "title", ascending: true)
        #expect(results.count == 1)
        #expect(results[0].title == "Bohemian Rhapsody")
    }

    // Searches with a prefix to verify autocomplete-style matching.
    @Test func fts5PrefixSearch() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Bohemian Rhapsody", artist: "Queen")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Hotel California", artist: "Eagles")
        try db.insert(&t1)
        try db.insert(&t2)

        let results = try db.fetchTracks(search: "boh", sortColumn: "title", ascending: true)
        #expect(results.count == 1)
        #expect(results[0].title == "Bohemian Rhapsody")
    }

    // Inserts a batch of tracks and verifies duplicates are silently ignored.
    @Test func batchInsertIgnoresDuplicates() throws {
        let db = try DatabaseService(inMemory: true)
        let tracks = [
            Track.fixture(fileURL: "/a.mp3", title: "Song A"),
            Track.fixture(fileURL: "/b.mp3", title: "Song B"),
            Track.fixture(fileURL: "/a.mp3", title: "Song A Duplicate"),
        ]
        try db.insertBatch(tracks)

        let all = try db.fetchTracks(search: "", sortColumn: "title", ascending: true)
        #expect(all.count == 2)
    }

    // Updates play stats and verifies they persist.
    @Test func updatePlayStats() throws {
        let db = try DatabaseService(inMemory: true)
        var track = Track.fixture()
        try db.insert(&track)

        let now = Date()
        try db.updatePlayStats(trackId: track.id!, playCount: 5, lastPlayedAt: now)

        let fetched = try db.fetchTracks(search: "", sortColumn: "title", ascending: true)
        #expect(fetched[0].playCount == 5)
        #expect(fetched[0].lastPlayedAt != nil)
    }

    // Validates that an invalid sort column falls back to "title".
    @Test func invalidSortColumnFallsBackToTitle() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Zebra")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Alpha")
        try db.insert(&t1)
        try db.insert(&t2)

        let result = try db.fetchTracks(search: "", sortColumn: "DROP TABLE tracks", ascending: true)
        #expect(result[0].title == "Alpha")
    }

    // Creates a playlist and verifies it can be fetched back.
    @Test func createAndFetchPlaylist() throws {
        let db = try DatabaseService(inMemory: true)
        let playlist = try db.createPlaylist(name: "Chill Vibes")

        #expect(playlist.id != nil)
        #expect(playlist.name == "Chill Vibes")

        let all = try db.fetchPlaylists()
        #expect(all.count == 1)
        #expect(all[0].name == "Chill Vibes")
    }

    // Renames a playlist and verifies the new name persists.
    @Test func renamePlaylist() throws {
        let db = try DatabaseService(inMemory: true)
        let playlist = try db.createPlaylist(name: "Old Name")
        try db.renamePlaylist(id: playlist.id!, name: "New Name")

        let all = try db.fetchPlaylists()
        #expect(all[0].name == "New Name")
    }

    // Deletes a playlist and verifies it's gone.
    @Test func deletePlaylist() throws {
        let db = try DatabaseService(inMemory: true)
        let playlist = try db.createPlaylist(name: "To Delete")
        try db.deletePlaylist(id: playlist.id!)

        let all = try db.fetchPlaylists()
        #expect(all.isEmpty)
    }

    // Adds tracks to a playlist and verifies order is preserved.
    @Test func addTracksToPlaylist() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Song A")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Song B")
        try db.insert(&t1)
        try db.insert(&t2)
        let playlist = try db.createPlaylist(name: "My Playlist")

        try db.addTrackToPlaylist(trackId: t1.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: t2.id!, playlistId: playlist.id!)

        let tracks = try db.fetchPlaylistTracks(playlistId: playlist.id!)
        #expect(tracks.count == 2)
        #expect(tracks[0].title == "Song A")
        #expect(tracks[1].title == "Song B")
    }

    // Removes a track from a playlist and verifies positions are compacted.
    @Test func removeTrackFromPlaylist() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Song A")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Song B")
        var t3 = Track.fixture(fileURL: "/c.mp3", title: "Song C")
        try db.insert(&t1)
        try db.insert(&t2)
        try db.insert(&t3)
        let playlist = try db.createPlaylist(name: "My Playlist")

        try db.addTrackToPlaylist(trackId: t1.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: t2.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: t3.id!, playlistId: playlist.id!)

        // Remove middle track
        try db.removeTrackFromPlaylist(trackId: t2.id!, playlistId: playlist.id!)

        let tracks = try db.fetchPlaylistTracks(playlistId: playlist.id!)
        #expect(tracks.count == 2)
        #expect(tracks[0].title == "Song A")
        #expect(tracks[1].title == "Song C")
    }

    // Reorders tracks in a playlist (move last to first).
    @Test func reorderPlaylistTracks() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Song A")
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Song B")
        var t3 = Track.fixture(fileURL: "/c.mp3", title: "Song C")
        try db.insert(&t1)
        try db.insert(&t2)
        try db.insert(&t3)
        let playlist = try db.createPlaylist(name: "My Playlist")

        try db.addTrackToPlaylist(trackId: t1.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: t2.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: t3.id!, playlistId: playlist.id!)

        // Move Song C (position 2) to position 0
        try db.reorderPlaylistTrack(playlistId: playlist.id!, fromPosition: 2, toPosition: 0)

        let tracks = try db.fetchPlaylistTracks(playlistId: playlist.id!)
        #expect(tracks[0].title == "Song C")
        #expect(tracks[1].title == "Song A")
        #expect(tracks[2].title == "Song B")
    }

    // Verifies that deleting a playlist cascades to playlist_tracks.
    @Test func deletePlaylistCascadesJoinRows() throws {
        let db = try DatabaseService(inMemory: true)
        var track = Track.fixture()
        try db.insert(&track)
        let playlist = try db.createPlaylist(name: "Temp")
        try db.addTrackToPlaylist(trackId: track.id!, playlistId: playlist.id!)

        try db.deletePlaylist(id: playlist.id!)

        // Re-create a playlist to verify no orphan join rows cause issues
        let playlist2 = try db.createPlaylist(name: "Temp2")
        let tracks = try db.fetchPlaylistTracks(playlistId: playlist2.id!)
        #expect(tracks.isEmpty)
    }

    // Verifies adding a duplicate track to a playlist is silently ignored.
    @Test func addDuplicateTrackToPlaylistIsIgnored() throws {
        let db = try DatabaseService(inMemory: true)
        var track = Track.fixture()
        try db.insert(&track)
        let playlist = try db.createPlaylist(name: "My Playlist")

        try db.addTrackToPlaylist(trackId: track.id!, playlistId: playlist.id!)
        try db.addTrackToPlaylist(trackId: track.id!, playlistId: playlist.id!)

        let tracks = try db.fetchPlaylistTracks(playlistId: playlist.id!)
        #expect(tracks.count == 1)
    }

    // Inserts three tracks with different dateAdded values and verifies fetchRecentlyAdded
    // returns them in descending order capped to the given limit.
    @Test func fetchRecentlyAdded() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", title: "Old", dateAdded: Date(timeIntervalSince1970: 1000))
        var t2 = Track.fixture(fileURL: "/b.mp3", title: "Mid", dateAdded: Date(timeIntervalSince1970: 2000))
        var t3 = Track.fixture(fileURL: "/c.mp3", title: "New", dateAdded: Date(timeIntervalSince1970: 3000))
        try db.insert(&t1)
        try db.insert(&t2)
        try db.insert(&t3)

        let recent = try db.fetchRecentlyAdded(limit: 2)
        #expect(recent.count == 2)
        #expect(recent[0].title == "New")
        #expect(recent[1].title == "Mid")
    }

    // Inserts two tracks with known durations and verifies totalDuration sums them correctly.
    @Test func totalDuration() throws {
        let db = try DatabaseService(inMemory: true)
        var t1 = Track.fixture(fileURL: "/a.mp3", duration: 120.0)
        var t2 = Track.fixture(fileURL: "/b.mp3", duration: 300.5)
        try db.insert(&t1)
        try db.insert(&t2)

        let total = try db.totalDuration()
        #expect(abs(total - 420.5) < 0.01)
    }

    // Verifies totalDuration returns 0 when the library is empty.
    @Test func totalDurationEmptyLibrary() throws {
        let db = try DatabaseService(inMemory: true)
        let total = try db.totalDuration()
        #expect(total == 0)
    }

    // Inserts 5 tracks, fetches 3 by ID, verifies only those 3 are returned
    // in the order of the requested IDs.
    @Test func fetchTracksByIds() throws {
        let db = try DatabaseService(inMemory: true)
        var tracks = (0..<5).map { i in
            Track.fixture(fileURL: "/track\(i).mp3", title: "Track \(i)")
        }
        for i in tracks.indices {
            try db.insert(&tracks[i])
        }

        let ids: [Int64] = [tracks[2].id!, tracks[0].id!, tracks[4].id!]
        let result = try db.fetchTracksByIds(ids)

        #expect(result.count == 3)
        #expect(result[0].id == tracks[2].id)
        #expect(result[1].id == tracks[0].id)
        #expect(result[2].id == tracks[4].id)
    }

    // Inserts tracks in different months and verifies fetchMonthlyAdditions returns
    // the correct per-month counts covering the requested range including empty months.
    // Uses a UTC calendar to match the implementation, which uses UTC month boundaries
    // because GRDB stores dates as UTC ISO8601 strings.
    @Test func fetchMonthlyAdditions() throws {
        let db = try DatabaseService(inMemory: true)
        var utcCal = Calendar(identifier: .gregorian)
        utcCal.timeZone = TimeZone(identifier: "UTC")!
        let now = Date()
        let thisMonth = utcCal.dateInterval(of: .month, for: now)!.start
        let twoMonthsAgo = utcCal.date(byAdding: .month, value: -2, to: thisMonth)!

        var t1 = Track.fixture(fileURL: "/a.mp3", dateAdded: thisMonth)
        var t2 = Track.fixture(fileURL: "/b.mp3", dateAdded: thisMonth.addingTimeInterval(86400))
        var t3 = Track.fixture(fileURL: "/c.mp3", dateAdded: twoMonthsAgo)
        try db.insert(&t1)
        try db.insert(&t2)
        try db.insert(&t3)

        let results = try db.fetchMonthlyAdditions(months: 3)
        #expect(results.count == 3)
        #expect(results[0].count == 1)
        #expect(results[1].count == 0)
        #expect(results[2].count == 2)
    }
}
