import Testing
import CoreGraphics
@testable import Alas

@Suite("ACPMessageList pagination")
struct ACPMessageListPaginationTests {
    @Test("top indicator is hidden when the full transcript is visible")
    func hiddenWhenFullyVisible() {
        #expect(ACPMessageList.topPaginationIndicator(
            visibleHead: 0,
            isBackfillingOlderMessages: false
        ) == .hidden)
    }

    @Test("top indicator keeps an invisible sentinel after backfill completes")
    func sentinelWhenOlderRowsAreAvailable() {
        #expect(ACPMessageList.topPaginationIndicator(
            visibleHead: 30,
            isBackfillingOlderMessages: false
        ) == .sentinel)
    }

    @Test("top indicator shows spinner only while older rows are backfilling")
    func spinnerWhileBackfilling() {
        #expect(ACPMessageList.topPaginationIndicator(
            visibleHead: 0,
            isBackfillingOlderMessages: true
        ) == .spinner)
        #expect(ACPMessageList.topPaginationIndicator(
            visibleHead: 30,
            isBackfillingOlderMessages: true
        ) == .spinner)
    }

    @Test("queue bubbles render pending items only")
    func queueBubblesRenderPendingItemsOnly() {
        #expect(ACPMessageList.shouldRenderQueueBubble(status: .pending))
        #expect(!ACPMessageList.shouldRenderQueueBubble(status: .sending))
    }

    @Test("queue drops only accept pending items onto pending targets")
    func queueDropsOnlyAcceptPendingItemsOntoPendingTargets() {
        #expect(ACPMessageList.canDropQueuedItem(sourceStatus: .pending, targetStatus: .pending))
        #expect(!ACPMessageList.canDropQueuedItem(sourceStatus: .sending, targetStatus: .pending))
        #expect(!ACPMessageList.canDropQueuedItem(sourceStatus: .pending, targetStatus: .sending))
        #expect(!ACPMessageList.canDropQueuedItem(sourceStatus: nil, targetStatus: .pending))
    }

    @Test("queue header count matches rendered queue bubbles")
    func queueHeaderCountMatchesRenderedQueueBubbles() {
        #expect(ACPMessageList.queueHeaderCount(statuses: []) == 0)
        #expect(ACPMessageList.queueHeaderCount(statuses: [.pending]) == 1)
        #expect(ACPMessageList.queueHeaderCount(statuses: [.sending]) == 0)
        #expect(ACPMessageList.queueHeaderCount(statuses: [.sending, .pending]) == 1)
    }

    @Test("content growth restores the tail when tail-follow is still enabled")
    func contentGrowthRestoresTailWhenFollowing() {
        let viewportHeight: CGFloat = 600
        let previousContentHeight: CGFloat = 5_000
        let newContentHeight: CGFloat = 5_220
        let oldTailOffset = previousContentHeight - viewportHeight

        #expect(ACPMessageList.shouldRestoreTailAfterContentGrowth(
            previousContentHeight: previousContentHeight,
            newContentHeight: newContentHeight,
            viewportHeight: viewportHeight,
            newMinY: oldTailOffset,
            followsTranscriptTail: true
        ))
    }

    @Test("content growth does not restore the tail after the user paused tail-follow")
    func contentGrowthDoesNotRestoreWhenTailFollowPaused() {
        #expect(!ACPMessageList.shouldRestoreTailAfterContentGrowth(
            previousContentHeight: 5_000,
            newContentHeight: 5_220,
            viewportHeight: 600,
            newMinY: 4_400,
            followsTranscriptTail: false
        ))
    }

    @Test("content growth does not issue an extra restore when already at the new tail")
    func contentGrowthDoesNotRestoreWhenAlreadyAtTail() {
        let viewportHeight: CGFloat = 600
        let newContentHeight: CGFloat = 5_220
        let newTailOffset = newContentHeight - viewportHeight

        #expect(!ACPMessageList.shouldRestoreTailAfterContentGrowth(
            previousContentHeight: 5_000,
            newContentHeight: newContentHeight,
            viewportHeight: viewportHeight,
            newMinY: newTailOffset,
            followsTranscriptTail: true
        ))
    }

    @Test("top visible anchor prefers the row crossing the viewport top")
    func topVisibleAnchorPrefersRowCrossingTop() {
        let frames: [String: CGRect] = [
            "above": CGRect(x: 0, y: -90, width: 100, height: 50),
            "crossing": CGRect(x: 0, y: -40, width: 100, height: 80),
            "below": CGRect(x: 0, y: 48, width: 100, height: 80)
        ]

        #expect(ACPMessageList.topVisibleAnchorID(in: frames) == "crossing")
    }

    @Test("top visible anchor uses first row below top when no row crosses")
    func topVisibleAnchorUsesFirstRowBelowTop() {
        let frames: [String: CGRect] = [
            "above": CGRect(x: 0, y: -90, width: 100, height: 50),
            "first": CGRect(x: 0, y: 16, width: 100, height: 80),
            "second": CGRect(x: 0, y: 120, width: 100, height: 80)
        ]

        #expect(ACPMessageList.topVisibleAnchorID(in: frames) == "first")
    }

    @Test("top visible scroll target ignores non-message targets")
    func topVisibleScrollTargetIgnoresNonMessageTargets() {
        #expect(ACPMessageList.topVisibleScrollTargetID(
            in: ["__pending_perm__", "message-2", "message-3"],
            visibleMessageIds: ["message-1", "message-2", "message-3"]
        ) == "message-2")
    }

    @Test("top visible scroll target returns nil without visible messages")
    func topVisibleScrollTargetReturnsNilWithoutMessages() {
        #expect(ACPMessageList.topVisibleScrollTargetID(
            in: ["__pending_question__", "__composer_spacer__"],
            visibleMessageIds: ["message-1"]
        ) == nil)
    }

    @Test("visible anchor memory accepts live anchors when the remembered id is stale")
    func visibleAnchorMemoryAcceptsStaleRememberedIDReplacement() {
        #expect(ACPMessageList.shouldRememberVisibleAnchor(
            "regenerated-2",
            rememberedAnchor: "old-2",
            restoredRememberedAnchor: nil,
            visibleMessageIds: ["regenerated-1", "regenerated-2", "regenerated-3"],
            isBackfillingOlderMessages: false
        ))
    }

    @Test("visible anchor memory waits while the remembered id is still rendered")
    func visibleAnchorMemoryWaitsForRenderedRememberedID() {
        #expect(!ACPMessageList.shouldRememberVisibleAnchor(
            "message-1",
            rememberedAnchor: "message-2",
            restoredRememberedAnchor: nil,
            visibleMessageIds: ["message-1", "message-2", "message-3"],
            isBackfillingOlderMessages: false
        ))
    }

    @Test("visible anchor memory accepts refreshed anchors after restoration")
    func visibleAnchorMemoryAcceptsAfterRestoration() {
        #expect(ACPMessageList.shouldRememberVisibleAnchor(
            "message-3",
            rememberedAnchor: "message-2",
            restoredRememberedAnchor: "message-2",
            visibleMessageIds: ["message-1", "message-2", "message-3"],
            isBackfillingOlderMessages: false
        ))
    }

    @Test("visible anchor memory waits while older messages are backfilling")
    func visibleAnchorMemoryWaitsDuringBackfill() {
        #expect(!ACPMessageList.shouldRememberVisibleAnchor(
            "tail-2",
            rememberedAnchor: "old-2",
            restoredRememberedAnchor: nil,
            visibleMessageIds: ["tail-1", "tail-2", "tail-3"],
            isBackfillingOlderMessages: true
        ))
    }
}
