//
//  DataTests.swift
//  YatteeTests
//
//  Tests for the local data persistence layer.
//

import Testing
import Foundation
@testable import Yattee

@MainActor
@Suite("Data Layer Tests")
struct DataTests {

    // MARK: - Watch Entry Tests

    @Suite("Watch Entry")
    struct WatchEntryTests {
        @Test("Progress calculation")
        @MainActor
        func progressCalculation() {
            let entry = WatchEntry(
                videoID: "test123",
                sourceRawValue: "youtube",
                title: "Test Video",
                authorName: "Test Channel",
                authorID: "channel123",
                duration: 600, // 10 minutes
                watchedSeconds: 300 // 5 minutes
            )

            #expect(entry.progress == 0.5)
        }

        @Test("Auto-finish at 90%")
        @MainActor
        func autoFinishAt90Percent() {
            let entry = WatchEntry(
                videoID: "test123",
                sourceRawValue: "youtube",
                title: "Test Video",
                authorName: "Test Channel",
                authorID: "channel123",
                duration: 100
            )

            #expect(!entry.isFinished)

            entry.updateProgress(seconds: 90)

            #expect(entry.isFinished)
            #expect(entry.progress >= 0.9)
        }

        @Test("Reset progress")
        @MainActor
        func resetProgress() {
            let entry = WatchEntry(
                videoID: "test123",
                sourceRawValue: "youtube",
                title: "Test Video",
                authorName: "Test Channel",
                authorID: "channel123",
                duration: 100,
                watchedSeconds: 90,
                isFinished: true
            )

            entry.resetProgress()

            #expect(entry.watchedSeconds == 0)
            #expect(!entry.isFinished)
        }

        @Test("Content source YouTube")
        @MainActor
        func contentSourceYouTube() {
            let entry = WatchEntry(
                videoID: "abc123",
                sourceRawValue: "global",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .global = entry.contentSource {
                // Expected
            } else {
                Issue.record("Expected global source")
            }
        }

        @Test("Content source Federated")
        @MainActor
        func contentSourceFederated() {
            let entry = WatchEntry(
                videoID: "uuid123",
                sourceRawValue: "federated",
                instanceURLString: "https://peertube.example.com",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .federated(_, let instance) = entry.contentSource {
                #expect(instance.host == "peertube.example.com")
            } else {
                Issue.record("Expected federated source")
            }
        }

        @Test("Remaining time formatting")
        @MainActor
        func remainingTimeFormatting() {
            let entry = WatchEntry(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 600, // 10 minutes
                watchedSeconds: 300 // 5 minutes watched
            )

            // 5 minutes remaining = "5:00"
            #expect(entry.remainingTime == "5:00")
        }

        @Test("Remaining time with seconds")
        @MainActor
        func remainingTimeWithSeconds() {
            let entry = WatchEntry(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 125, // 2:05
                watchedSeconds: 60 // 1 minute watched
            )

            // 65 seconds remaining = "1:05"
            #expect(entry.remainingTime == "1:05")
        }

        @Test("Thumbnail URL conversion")
        @MainActor
        func thumbnailURL() {
            let entry = WatchEntry(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: "https://example.com/thumb.jpg"
            )

            #expect(entry.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
        }

        @Test("Thumbnail URL nil for invalid string")
        @MainActor
        func thumbnailURLNil() {
            let entry = WatchEntry(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: nil
            )

            #expect(entry.thumbnailURL == nil)
        }

        @Test("Mark as finished")
        @MainActor
        func markAsFinished() {
            let entry = WatchEntry(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            #expect(!entry.isFinished)
            entry.markAsFinished()
            #expect(entry.isFinished)
        }
    }

    // MARK: - Bookmark Tests

    @Suite("Bookmark")
    struct BookmarkTests {
        @Test("Formatted duration hours")
        @MainActor
        func formattedDurationHours() {
            let bookmark = Bookmark(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Long Video",
                authorName: "Channel",
                authorID: "ch1",
                duration: 3661 // 1:01:01
            )

            #expect(bookmark.formattedDuration == "1:01:01")
        }

        @Test("Formatted duration minutes")
        @MainActor
        func formattedDurationMinutes() {
            let bookmark = Bookmark(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Short Video",
                authorName: "Channel",
                authorID: "ch1",
                duration: 125 // 2:05
            )

            #expect(bookmark.formattedDuration == "2:05")
        }

        @Test("Live shows LIVE")
        @MainActor
        func liveShowsLive() {
            let bookmark = Bookmark(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Live Stream",
                authorName: "Channel",
                authorID: "ch1",
                duration: 0,
                isLive: true
            )

            #expect(bookmark.formattedDuration == "LIVE")
        }

        @Test("Content source Global")
        @MainActor
        func contentSourceGlobal() {
            let bookmark = Bookmark(
                videoID: "abc123",
                sourceRawValue: "global",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .global = bookmark.contentSource {
                // Expected
            } else {
                Issue.record("Expected global source")
            }
        }

        @Test("Content source Federated")
        @MainActor
        func contentSourceFederated() {
            let bookmark = Bookmark(
                videoID: "uuid123",
                sourceRawValue: "federated",
                instanceURLString: "https://peertube.example.com",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .federated(_, let instance) = bookmark.contentSource {
                #expect(instance.host == "peertube.example.com")
            } else {
                Issue.record("Expected federated source")
            }
        }

        @Test("Thumbnail URL conversion")
        @MainActor
        func thumbnailURL() {
            let bookmark = Bookmark(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: "https://example.com/thumb.jpg"
            )

            #expect(bookmark.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
        }

        @Test("Zero duration shows empty string")
        @MainActor
        func zeroDuration() {
            let bookmark = Bookmark(
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 0,
                isLive: false
            )

            #expect(bookmark.formattedDuration == "")
        }
    }

    // MARK: - Local Playlist Tests

    @Suite("Local Playlist")
    struct LocalPlaylistTests {
        @Test("Video count")
        @MainActor
        func videoCount() {
            let playlist = LocalPlaylist(title: "My Playlist")

            #expect(playlist.videoCount == 0)
        }

        @Test("Total duration formatting")
        @MainActor
        func totalDurationFormatting() {
            let playlist = LocalPlaylist(title: "My Playlist")

            // Empty playlist
            #expect(playlist.formattedTotalDuration == "0 min")
        }

        @Test("Total duration with hours")
        @MainActor
        func totalDurationWithHours() {
            let playlist = LocalPlaylist(title: "Long Playlist")

            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "video1",
                sourceRawValue: "youtube",
                title: "Long Video",
                authorName: "Channel",
                authorID: "ch1",
                duration: 7200 // 2 hours
            )
            item.playlist = playlist
            playlist.items?.append(item)

            #expect(playlist.formattedTotalDuration == "2h 0m")
        }

        @Test("Contains video check")
        @MainActor
        func containsVideoCheck() {
            let playlist = LocalPlaylist(title: "Test")

            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "abc123",
                sourceRawValue: "youtube",
                title: "Video",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )
            item.playlist = playlist
            playlist.items?.append(item)

            #expect(playlist.contains(videoID: "abc123"))
            #expect(!playlist.contains(videoID: "xyz789"))
        }

        @Test("Sorted items by order")
        @MainActor
        func sortedItems() {
            let playlist = LocalPlaylist(title: "Test")

            // Add items out of order
            let item2 = LocalPlaylistItem(
                sortOrder: 2,
                videoID: "video2",
                sourceRawValue: "youtube",
                title: "Second",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )
            let item1 = LocalPlaylistItem(
                sortOrder: 1,
                videoID: "video1",
                sourceRawValue: "youtube",
                title: "First",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )
            let item3 = LocalPlaylistItem(
                sortOrder: 3,
                videoID: "video3",
                sourceRawValue: "youtube",
                title: "Third",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            item1.playlist = playlist
            item2.playlist = playlist
            item3.playlist = playlist
            playlist.items?.append(item2)
            playlist.items?.append(item1)
            playlist.items?.append(item3)

            let sorted = playlist.sortedItems
            #expect(sorted.count == 3)
            #expect(sorted[0].videoID == "video1")
            #expect(sorted[1].videoID == "video2")
            #expect(sorted[2].videoID == "video3")
        }

        @Test("Thumbnail URL from first sorted item")
        @MainActor
        func thumbnailURL() {
            let playlist = LocalPlaylist(title: "Test")

            let item1 = LocalPlaylistItem(
                sortOrder: 1,
                videoID: "video1",
                sourceRawValue: "youtube",
                title: "First",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: "https://example.com/thumb1.jpg"
            )
            let item2 = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "video2",
                sourceRawValue: "youtube",
                title: "Actually First",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: "https://example.com/thumb2.jpg"
            )

            item1.playlist = playlist
            item2.playlist = playlist
            playlist.items?.append(item1)
            playlist.items?.append(item2)

            // Should get thumbnail from item with lowest sortOrder
            #expect(playlist.thumbnailURL?.absoluteString == "https://example.com/thumb2.jpg")
        }

        @Test("Items is optional for CloudKit compatibility")
        @MainActor
        func itemsOptional() {
            let playlist = LocalPlaylist(title: "Empty")
            // items should be initialized as empty array, not nil
            #expect(playlist.items != nil)
            #expect(playlist.items?.isEmpty == true)
        }
    }

    // MARK: - Subscription Tests

    @Suite("Subscription")
    struct SubscriptionTests {
        @Test("Formatted subscriber count")
        @MainActor
        func formattedSubscriberCount() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "youtube",
                name: "Popular Channel",
                subscriberCount: 1_500_000
            )

            #expect(sub.formattedSubscriberCount == "1.5M")
        }

        @Test("No subscriber count")
        @MainActor
        func noSubscriberCount() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "youtube",
                name: "New Channel"
            )

            #expect(sub.formattedSubscriberCount == nil)
        }

        @Test("Content source Global")
        @MainActor
        func contentSourceGlobal() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "global",
                name: "Channel"
            )

            if case .global = sub.contentSource {
                // Expected
            } else {
                Issue.record("Expected global source")
            }
        }

        @Test("Content source Federated")
        @MainActor
        func contentSourceFederated() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "federated",
                instanceURLString: "https://peertube.example.com",
                name: "Channel"
            )

            if case .federated(_, let instance) = sub.contentSource {
                #expect(instance.host == "peertube.example.com")
            } else {
                Issue.record("Expected federated source")
            }
        }

        @Test("Avatar URL conversion")
        @MainActor
        func avatarURL() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "youtube",
                name: "Channel",
                avatarURLString: "https://example.com/avatar.jpg"
            )

            #expect(sub.avatarURL?.absoluteString == "https://example.com/avatar.jpg")
        }

        @Test("Banner URL conversion")
        @MainActor
        func bannerURL() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "youtube",
                name: "Channel",
                bannerURLString: "https://example.com/banner.jpg"
            )

            #expect(sub.bannerURL?.absoluteString == "https://example.com/banner.jpg")
        }

        @Test("Update from channel")
        @MainActor
        func updateFromChannel() {
            let sub = Subscription(
                channelID: "ch123",
                sourceRawValue: "youtube",
                name: "Old Name",
                subscriberCount: 1000
            )

            let channel = Channel(
                id: .global("ch123"),
                name: "New Name",
                description: "Updated description",
                subscriberCount: 2000,
                thumbnailURL: URL(string: "https://example.com/new-avatar.jpg"),
                bannerURL: URL(string: "https://example.com/new-banner.jpg"),
                isVerified: true
            )

            sub.update(from: channel)

            #expect(sub.name == "New Name")
            #expect(sub.channelDescription == "Updated description")
            #expect(sub.subscriberCount == 2000)
            #expect(sub.avatarURLString == "https://example.com/new-avatar.jpg")
            #expect(sub.bannerURLString == "https://example.com/new-banner.jpg")
            #expect(sub.isVerified == true)
        }
    }

    // MARK: - LocalPlaylistItem Tests

    @Suite("LocalPlaylistItem")
    struct LocalPlaylistItemTests {
        @Test("Content source Global")
        @MainActor
        func contentSourceGlobal() {
            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "abc123",
                sourceRawValue: "global",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .global = item.contentSource {
                // Expected
            } else {
                Issue.record("Expected global source")
            }
        }

        @Test("Content source Federated")
        @MainActor
        func contentSourceFederated() {
            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "uuid123",
                sourceRawValue: "federated",
                instanceURLString: "https://peertube.example.com",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            if case .federated(_, let instance) = item.contentSource {
                #expect(instance.host == "peertube.example.com")
            } else {
                Issue.record("Expected federated source")
            }
        }

        @Test("Thumbnail URL conversion")
        @MainActor
        func thumbnailURL() {
            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "abc123",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100,
                thumbnailURLString: "https://example.com/thumb.jpg"
            )

            #expect(item.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
        }

        @Test("Default values for CloudKit compatibility")
        @MainActor
        func defaultValues() {
            let item = LocalPlaylistItem(
                sortOrder: 0,
                videoID: "test",
                sourceRawValue: "youtube",
                title: "Test",
                authorName: "Channel",
                authorID: "ch1",
                duration: 100
            )

            // Check default values are set
            #expect(item.isLive == false)
            #expect(item.thumbnailURLString == nil)
            #expect(item.instanceURLString == nil)
            #expect(item.peertubeUUID == nil)
        }
    }

    // MARK: - DataManager Tests

    @Suite("DataManager")
    struct DataManagerTests {
        @Test("Watch progress round trip")
        @MainActor
        func watchProgressRoundTrip() async throws {
            let manager = try DataManager(inMemory: true)

            // Create a test video
            let video = Video(
                id: .global("testVideo123"),
                title: "Test Video",
                description: nil,
                author: Author(id: "ch1", name: "Channel"),
                duration: 600,
                publishedAt: nil,
                publishedText: nil,
                viewCount: nil,
                likeCount: nil,
                thumbnails: [],
                isLive: false,
                isUpcoming: false,
                scheduledStartTime: nil
            )

            // Record progress
            manager.updateWatchProgress(for: video, seconds: 300)

            // Retrieve progress
            let progress = manager.watchProgress(for: "testVideo123")

            #expect(progress == 300)
        }

        @Test("Live stream is recorded in history without a resume position")
        @MainActor
        func liveStreamStoresNoProgress() async throws {
            let manager = try DataManager(inMemory: true)

            let live = Video(
                id: .global("liveStream1"),
                title: "Live Stream",
                description: nil,
                author: Author(id: "ch1", name: "Channel"),
                duration: 0,
                publishedAt: nil,
                publishedText: nil,
                viewCount: nil,
                likeCount: nil,
                thumbnails: [],
                isLive: true,
                isUpcoming: false,
                scheduledStartTime: nil
            )

            manager.updateWatchProgress(for: live, seconds: 1800, duration: 1800)

            // The entry exists so the stream shows up in History...
            let entry = try #require(manager.watchEntry(for: "liveStream1"))
            #expect(entry.isLive)
            // ...but carries nothing that could be offered as a resume point
            #expect(entry.watchedSeconds == 0)
            #expect(entry.duration == 0)
            #expect(entry.progress == 0)
            #expect(!entry.isFinished)
            #expect(entry.toVideo().isLive)
        }

        @Test("Piped live sentinel duration never reaches storage")
        @MainActor
        func liveStreamNegativeDurationSentinel() async throws {
            let manager = try DataManager(inMemory: true)

            // Piped encodes "live" as duration == -1 and passes it straight into Video
            let live = Video(
                id: .global("liveStream2"),
                title: "Piped Live",
                description: nil,
                author: Author(id: "ch1", name: "Channel"),
                duration: -1,
                publishedAt: nil,
                publishedText: nil,
                viewCount: nil,
                likeCount: nil,
                thumbnails: [],
                isLive: true,
                isUpcoming: false,
                scheduledStartTime: nil
            )

            manager.updateWatchProgressLocal(for: live, seconds: 120, duration: 120)

            let entry = try #require(manager.watchEntry(for: "liveStream2"))
            #expect(entry.duration == 0)
            #expect(entry.remainingTime == "0:00")
        }

        @Test("Entry heals once a former live stream becomes a VOD")
        @MainActor
        func liveEntryBecomesVOD() async throws {
            let manager = try DataManager(inMemory: true)

            let makeVideo: (Bool, TimeInterval) -> Video = { isLive, duration in
                Video(
                    id: .global("liveThenVOD"),
                    title: "Stream",
                    description: nil,
                    author: Author(id: "ch1", name: "Channel"),
                    duration: duration,
                    publishedAt: nil,
                    publishedText: nil,
                    viewCount: nil,
                    likeCount: nil,
                    thumbnails: [],
                    isLive: isLive,
                    isUpcoming: false,
                    scheduledStartTime: nil
                )
            }

            manager.updateWatchProgress(for: makeVideo(true, 0), seconds: 900, duration: 900)
            #expect(manager.watchProgress(for: "liveThenVOD") == 0)

            // Same video watched later as a regular recording
            manager.updateWatchProgress(for: makeVideo(false, 1200), seconds: 300, duration: 1200)

            let entry = try #require(manager.watchEntry(for: "liveThenVOD"))
            #expect(!entry.isLive)
            #expect(entry.watchedSeconds == 300)
            #expect(entry.duration == 1200)
            #expect(entry.progress == 0.25)
        }

        @Test("Healing save past 90% marks a former live entry finished")
        @MainActor
        func liveEntryHealAutoFinishes() async throws {
            let manager = try DataManager(inMemory: true)

            let makeVideo: (Bool, TimeInterval) -> Video = { isLive, duration in
                Video(
                    id: .global("liveThenFinished"),
                    title: "Stream",
                    description: nil,
                    author: Author(id: "ch1", name: "Channel"),
                    duration: duration,
                    publishedAt: nil,
                    publishedText: nil,
                    viewCount: nil,
                    likeCount: nil,
                    thumbnails: [],
                    isLive: isLive,
                    isUpcoming: false,
                    scheduledStartTime: nil
                )
            }

            manager.updateWatchProgress(for: makeVideo(true, 0), seconds: 900, duration: 900)

            // First VOD save lands past the 90% threshold - the auto-finish check
            // must see the cleared isLive flag, not the hard-0 live progress
            manager.updateWatchProgress(for: makeVideo(false, 1000), seconds: 950, duration: 1000)

            let entry = try #require(manager.watchEntry(for: "liveThenFinished"))
            #expect(!entry.isLive)
            #expect(entry.isFinished)
        }

        @Test("Bookmark toggle")
        @MainActor
        func bookmarkToggle() async throws {
            let manager = try DataManager(inMemory: true)

            let video = Video(
                id: .global("bookmarkTest"),
                title: "Bookmark Me",
                description: nil,
                author: Author(id: "ch1", name: "Channel"),
                duration: 300,
                publishedAt: nil,
                publishedText: nil,
                viewCount: nil,
                likeCount: nil,
                thumbnails: [],
                isLive: false,
                isUpcoming: false,
                scheduledStartTime: nil
            )

            #expect(!manager.isBookmarked(videoID: "bookmarkTest"))

            manager.addBookmark(for: video)
            #expect(manager.isBookmarked(videoID: "bookmarkTest"))

            manager.removeBookmark(for: "bookmarkTest")
            #expect(!manager.isBookmarked(videoID: "bookmarkTest"))
        }

        @Test("Create and delete playlist")
        @MainActor
        func createAndDeletePlaylist() async throws {
            let manager = try DataManager(inMemory: true)

            let playlist = manager.createPlaylist(title: "Test Playlist", description: "A test")

            #expect(playlist.title == "Test Playlist")
            #expect(playlist.playlistDescription == "A test")

            var playlists = manager.playlists()
            #expect(playlists.count == 1)

            manager.deletePlaylist(playlist)

            playlists = manager.playlists()
            #expect(playlists.count == 0)
        }

        @Test("Subscription management")
        @MainActor
        func subscriptionManagement() async throws {
            let manager = try DataManager(inMemory: true)

            let channel = Channel(
                id: .global("testChannel"),
                name: "Test Channel",
                description: "A test channel",
                subscriberCount: 10000,
                thumbnailURL: nil
            )

            #expect(!manager.isSubscribed(to: "testChannel"))

            manager.subscribe(to: channel)
            #expect(manager.isSubscribed(to: "testChannel"))

            let subs = manager.subscriptions()
            #expect(subs.count == 1)
            #expect(subs.first?.name == "Test Channel")

            manager.unsubscribe(from: "testChannel")
            #expect(!manager.isSubscribed(to: "testChannel"))
        }

        @Test("Watch history ordering")
        @MainActor
        func watchHistoryOrdering() async throws {
            let manager = try DataManager(inMemory: true)

            // Create multiple videos
            for i in 1...3 {
                let video = Video(
                    id: .global("video\(i)"),
                    title: "Video \(i)",
                    description: nil,
                    author: Author(id: "ch1", name: "Channel"),
                    duration: 100,
                    publishedAt: nil,
                    publishedText: nil,
                    viewCount: nil,
                    likeCount: nil,
                    thumbnails: [],
                    isLive: false,
                    isUpcoming: false,
                    scheduledStartTime: nil
                )
                manager.updateWatchProgress(for: video, seconds: Double(i * 10))

                // Small delay to ensure different timestamps
                try await Task.sleep(for: .milliseconds(10))
            }

            let history = manager.watchHistory()

            #expect(history.count == 3)
            // Most recent should be first
            #expect(history.first?.videoID == "video3")
        }
    }
}
