// swiftlint:disable file_length
import Foundation
import Testing
@testable import Forji

// MARK: - Test helpers

/// Waits on MainActor until a condition is true, yielding between checks.
@MainActor
private func yieldUntil(_ condition: @MainActor () -> Bool) async {
    for _ in 0 ..< 500 {
        if condition() { return }
        await Task.yield()
    }
}

/// A fetch whose completion the test controls via continuations.
@MainActor
private final class ControllableFetch {
    private(set) var callCount = 0
    private var continuation: CheckedContinuation<[String], any Error>?
    var isPending: Bool { continuation != nil }

    func fetch(page: Int, limit: Int) async throws -> [String] {
        callCount += 1
        return try await withCheckedThrowingContinuation { cont in
            self.continuation = cont
        }
    }

    func complete(returning items: [String]) {
        let cont = continuation
        continuation = nil
        cont?.resume(returning: items)
    }

    func complete(throwing error: any Error) {
        let cont = continuation
        continuation = nil
        cont?.resume(throwing: error)
    }
}

// MARK: - Basic reload

struct PaginationStateBasicTests {

    @Test @MainActor func reloadSetsItems() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in ["a", "b", "c"] }.value
        #expect(pagination.items == ["a", "b", "c"])
        #expect(!pagination.isLoading)
        #expect(!pagination.hasMore) // 3 < pageSize 5
    }

    @Test @MainActor func reloadSetsHasMoreWhenFull() async {
        let pagination = PaginationState<String>(pageSize: 3)
        await pagination.reload { _, _ in ["a", "b", "c"] }.value
        #expect(pagination.hasMore) // 3 >= pageSize 3
    }

    @Test @MainActor func reloadReplacesExistingItems() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in ["old"] }.value
        await pagination.reload { _, _ in ["new"] }.value
        #expect(pagination.items == ["new"])
    }

    @Test @MainActor func reloadError() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in throw URLError(.badServerResponse) }.value
        #expect(pagination.items.isEmpty)
        #expect(pagination.showError)
        #expect(pagination.errorMessage != nil)
        #expect(!pagination.isLoading)
    }

    @Test @MainActor func reloadClearsErrorState() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in throw URLError(.badServerResponse) }.value
        #expect(pagination.showError)

        await pagination.reload { _, _ in ["ok"] }.value
        #expect(!pagination.showError)
        #expect(pagination.items == ["ok"])
    }

    @Test @MainActor func clearItemsEmptiesImmediately() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in ["a", "b"] }.value

        let fetcher = ControllableFetch()
        let task = pagination.reload(clearItems: true) { page, limit in
            try await fetcher.fetch(page: page, limit: limit)
        }
        // Items should be cleared synchronously before the fetch starts
        #expect(pagination.items.isEmpty)
        #expect(pagination.isLoading)

        await yieldUntil { fetcher.isPending }
        fetcher.complete(returning: ["c"])
        await task.value
        #expect(pagination.items == ["c"])
        #expect(!pagination.isLoading)
    }
}

// MARK: - Cancellation

struct PaginationStateCancellationTests {

    @Test @MainActor func cancellationErrorIgnored() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in ["initial"] }.value

        await pagination.reload { _, _ in throw CancellationError() }.value

        // Items from new reload are empty since it threw before returning items
        // But cancellation should not surface an error
        #expect(!pagination.showError)
        #expect(!pagination.isLoading)
    }

    @Test @MainActor func urlErrorCancelledIgnored() async {
        let pagination = PaginationState<String>(pageSize: 20)

        await pagination.reload { _, _ in throw URLError(.cancelled) }.value

        #expect(pagination.items.isEmpty)
        #expect(!pagination.showError)
        #expect(!pagination.isLoading)
    }
}

// MARK: - Concurrent reloads (internal task cancellation)

struct PaginationStateConcurrentTests {

    @Test @MainActor func secondReloadCancelsFirst() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let fetcherA = ControllableFetch()
        let fetcherB = ControllableFetch()

        // Start reload A
        pagination.reload { page, limit in
            try await fetcherA.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherA.isPending }

        // Start reload B, cancels A's internal task
        let taskB = pagination.reload { page, limit in
            try await fetcherB.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherB.isPending }

        // Complete both, only B's results should be applied
        fetcherA.complete(returning: ["stale"])
        fetcherB.complete(returning: ["fresh"])
        await taskB.value

        #expect(pagination.items == ["fresh"])
        #expect(!pagination.isLoading)
    }

    @Test @MainActor func staleLoadCompletingFirstIsDiscarded() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let fetcherA = ControllableFetch()
        let fetcherB = ControllableFetch()

        pagination.reload { page, limit in
            try await fetcherA.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherA.isPending }

        let taskB = pagination.reload { page, limit in
            try await fetcherB.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherB.isPending }

        // Complete A first (stale, its task was cancelled), must be discarded
        fetcherA.complete(returning: ["stale"])
        await yieldUntil { fetcherA.callCount == 1 }

        #expect(pagination.items.isEmpty) // B hasn't completed yet
        #expect(pagination.isLoading) // B is still loading

        // Complete B (fresh)
        fetcherB.complete(returning: ["fresh"])
        await taskB.value

        #expect(pagination.items == ["fresh"])
        #expect(!pagination.isLoading)
    }

    @Test @MainActor func threeOverlappingReloadsOnlyLatestApplied() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let fetcherA = ControllableFetch()
        let fetcherB = ControllableFetch()
        let fetcherC = ControllableFetch()

        pagination.reload { page, limit in
            try await fetcherA.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherA.isPending }

        pagination.reload { page, limit in
            try await fetcherB.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherB.isPending }

        let taskC = pagination.reload { page, limit in
            try await fetcherC.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherC.isPending }

        // Complete in order A, B, C, only C should be applied
        fetcherA.complete(returning: ["a"])
        fetcherB.complete(returning: ["b"])
        fetcherC.complete(returning: ["c"])
        await taskC.value

        #expect(pagination.items == ["c"])
        #expect(!pagination.isLoading)
    }

    @Test @MainActor func staleErrorDoesNotSurface() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let fetcherA = ControllableFetch()
        let fetcherB = ControllableFetch()

        pagination.reload { page, limit in
            try await fetcherA.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherA.isPending }

        let taskB = pagination.reload { page, limit in
            try await fetcherB.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherB.isPending }

        // A fails with error, but its task was cancelled, so no alert
        fetcherA.complete(throwing: URLError(.badServerResponse))
        fetcherB.complete(returning: ["ok"])
        await taskB.value

        #expect(pagination.items == ["ok"])
        #expect(!pagination.showError)
    }

    @Test @MainActor func isLoadingStaysTrueWhileLatestIsInFlight() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let fetcherA = ControllableFetch()
        let fetcherB = ControllableFetch()

        pagination.reload { page, limit in
            try await fetcherA.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherA.isPending }
        #expect(pagination.isLoading)

        let taskB = pagination.reload { page, limit in
            try await fetcherB.fetch(page: page, limit: limit)
        }
        await yieldUntil { fetcherB.isPending }
        #expect(pagination.isLoading)

        // Complete stale A, isLoading must REMAIN true (B is still in flight)
        fetcherA.complete(returning: ["stale"])
        await yieldUntil { fetcherA.callCount == 1 }
        #expect(pagination.isLoading, "isLoading must stay true while latest reload (B) is pending")

        fetcherB.complete(returning: ["fresh"])
        await taskB.value
        #expect(!pagination.isLoading)
    }

    /// Simulates the exact pattern from SearchableOverviewView:
    /// .task fires reload → user changes filter → onChange fires reload (cancels previous)
    @Test @MainActor func simulatedViewFilterChange() async {
        let pagination = PaginationState<String>(pageSize: 20)
        let initialFetcher = ControllableFetch()
        let filterFetcher = ControllableFetch()

        // Simulate .task { pagination.reload { ... } }
        pagination.reload { page, limit in
            try await initialFetcher.fetch(page: page, limit: limit)
        }
        await yieldUntil { initialFetcher.isPending }

        // Simulate onChange: user changed filter, just call reload again
        let filterTask = pagination.reload(clearItems: true) { page, limit in
            try await filterFetcher.fetch(page: page, limit: limit)
        }
        await yieldUntil { filterFetcher.isPending }

        // Initial load completes (stale, its task was cancelled)
        initialFetcher.complete(returning: ["stale-created-results"])
        // Filter load completes with correct results
        filterFetcher.complete(returning: ["fresh-involved-results"])

        await filterTask.value

        #expect(pagination.items == ["fresh-involved-results"])
        #expect(!pagination.isLoading)
        #expect(!pagination.showError)
    }
}

// MARK: - hasLoaded / invalidate

struct PaginationStateHasLoadedTests {

    @Test @MainActor func hasLoadedIsFalseInitially() {
        let pagination = PaginationState<String>(pageSize: 5)
        #expect(!pagination.hasLoaded)
    }

    @Test @MainActor func hasLoadedTrueAfterSuccessfulReload() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in ["a"] }.value
        #expect(pagination.hasLoaded)
    }

    @Test @MainActor func hasLoadedFalseAfterFailedReload() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in throw URLError(.badServerResponse) }.value
        #expect(!pagination.hasLoaded)
    }

    @Test @MainActor func invalidateResetsHasLoaded() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in ["a"] }.value
        #expect(pagination.hasLoaded)

        pagination.invalidate()
        #expect(!pagination.hasLoaded)
    }

    @Test @MainActor func invalidateThenReloadCycle() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in ["a"] }.value
        #expect(pagination.hasLoaded)

        pagination.invalidate()
        #expect(!pagination.hasLoaded)

        await pagination.reload { _, _ in ["b"] }.value
        #expect(pagination.hasLoaded)
        #expect(pagination.items == ["b"])
    }

    @Test @MainActor func loadMoreDoesNotSetHasLoaded() async {
        let pagination = PaginationState<String>(pageSize: 2)
        // hasLoaded is false before any reload
        #expect(!pagination.hasLoaded)

        // loadMore should not set hasLoaded
        await pagination.loadMore { _, _ in ["a", "b"] }
        #expect(!pagination.hasLoaded)
    }
}

// MARK: - notFound

struct PaginationStateNotFoundTests {

    @Test @MainActor func notFoundFalseInitially() {
        let pagination = PaginationState<String>(pageSize: 5)
        #expect(!pagination.notFound)
    }

    @Test @MainActor func notFoundSetByFetchClosureShowsNoAlert() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in
            pagination.notFound = true
            return []
        }.value
        #expect(pagination.notFound)
        #expect(pagination.items.isEmpty)
        #expect(!pagination.showError)
        #expect(pagination.hasLoaded)
    }

    @Test @MainActor func reloadClearsNotFound() async {
        let pagination = PaginationState<String>(pageSize: 20)
        await pagination.reload { _, _ in
            pagination.notFound = true
            return []
        }.value
        #expect(pagination.notFound)

        await pagination.reload { _, _ in ["ok"] }.value
        #expect(!pagination.notFound)
        #expect(pagination.items == ["ok"])
    }
}

// MARK: - loadMore

struct PaginationStateLoadMoreTests {

    @Test @MainActor func loadMoreAppendsItems() async {
        let pagination = PaginationState<String>(pageSize: 2)
        await pagination.reload { _, _ in ["a", "b"] }.value
        #expect(pagination.hasMore)

        await pagination.loadMore { page, _ in
            #expect(page == 2)
            return ["c", "d"]
        }
        #expect(pagination.items == ["a", "b", "c", "d"])
    }

    @Test @MainActor func loadMoreStopsWhenNoMore() async {
        let pagination = PaginationState<String>(pageSize: 5)
        await pagination.reload { _, _ in ["a", "b", "c", "d", "e"] }.value

        await pagination.loadMore { _, _ in ["f"] } // 1 < pageSize -> no more
        #expect(!pagination.hasMore)
        #expect(pagination.items == ["a", "b", "c", "d", "e", "f"])
    }

    @Test @MainActor func loadMoreSkipsWhenAlreadyLoading() async {
        let pagination = PaginationState<String>(pageSize: 2)
        await pagination.reload { _, _ in ["a", "b"] }.value

        let fetcher = ControllableFetch()
        let firstMore = Task {
            await pagination.loadMore { page, limit in
                try await fetcher.fetch(page: page, limit: limit)
            }
        }
        await yieldUntil { fetcher.isPending }

        // Second loadMore should bail because isLoading is true
        var secondCalled = false
        await pagination.loadMore { _, _ in
            secondCalled = true
            return ["should-not-run"]
        }
        #expect(!secondCalled)

        fetcher.complete(returning: ["c"])
        await firstMore.value
    }

    @Test @MainActor func loadMoreDiscardedWhenReloadSupersedes() async {
        let pagination = PaginationState<String>(pageSize: 2)
        await pagination.reload { _, _ in ["a", "b"] }.value

        let moreFetcher = ControllableFetch()
        let moreTask = Task {
            await pagination.loadMore { page, limit in
                try await moreFetcher.fetch(page: page, limit: limit)
            }
        }
        await yieldUntil { moreFetcher.isPending }

        // A reload supersedes the in-flight loadMore (cancels its task)
        let freshFetcher = ControllableFetch()
        let reloadTask = pagination.reload { page, limit in
            try await freshFetcher.fetch(page: page, limit: limit)
        }
        await yieldUntil { freshFetcher.isPending }

        // loadMore completes, should be discarded (task was cancelled)
        moreFetcher.complete(returning: ["stale-more"])
        await moreTask.value

        // reload completes, should be applied
        freshFetcher.complete(returning: ["fresh"])
        await reloadTask.value

        #expect(pagination.items == ["fresh"])
        #expect(!pagination.isLoading)
    }
}

// MARK: - Keyed de-duplication

struct PaginationStateDedupeTests {

    @Test @MainActor func loadMoreFiltersKeysSeenOnEarlierPages() async {
        let pagination = PaginationState<String>(pageSize: 2)
        pagination.dedupeKey = { $0 }
        await pagination.reload { _, _ in ["a", "b"] }.value

        // "b" resurfaces on page 2, only "c" is new
        await pagination.loadMore { _, _ in ["b", "c"] }
        #expect(pagination.items == ["a", "b", "c"])
        #expect(pagination.hasMore) // the page contributed a new item
    }

    @Test @MainActor func hasMoreFollowsNewItemsNotPageSize() async {
        let pagination = PaginationState<String>(pageSize: 5)
        pagination.dedupeKey = { $0 }
        await pagination.reload { _, _ in ["a", "b"] }.value
        #expect(pagination.hasMore) // 2 < pageSize 5, but the page contributed new items
    }

    @Test @MainActor func hasMoreFalseWhenFullPageIsAllDuplicates() async {
        let pagination = PaginationState<String>(pageSize: 2)
        pagination.dedupeKey = { $0 }
        await pagination.reload { _, _ in ["a", "b"] }.value
        #expect(pagination.hasMore)

        // 2 >= pageSize 2, but every key was already seen -> pagination ends
        await pagination.loadMore { _, _ in ["a", "b"] }
        #expect(pagination.items == ["a", "b"])
        #expect(!pagination.hasMore)
    }

    @Test @MainActor func reloadResetsSeenKeys() async {
        let pagination = PaginationState<String>(pageSize: 2)
        pagination.dedupeKey = { $0 }
        await pagination.reload { _, _ in ["a", "b"] }.value
        await pagination.loadMore { _, _ in ["b", "c"] }
        #expect(pagination.items == ["a", "b", "c"])

        // A fresh reload must accept keys seen in the previous cycle
        await pagination.reload { _, _ in ["a", "b"] }.value
        #expect(pagination.items == ["a", "b"])
    }
}
