import Foundation
import NucleicCore
import Testing

@testable import NucleicApp

@Suite("Transcript render batching")
struct TranscriptRenderSegmenterTests {
    @Test func onlyLongHistoriesUseNativeVirtualization() {
        #expect(!TranscriptRenderSegmenter.shouldVirtualize(
            rowCount: TranscriptRenderSegmenter.virtualizationThreshold))
        #expect(TranscriptRenderSegmenter.shouldVirtualize(
            rowCount: TranscriptRenderSegmenter.virtualizationThreshold + 1))
    }

    @Test func renderedTailIncludesAStandaloneLogNoteAfterTheLastMessage() {
        let session = SessionID(rawValue: "tail-note")
        let items: [TranscriptItem] = [
            .assistant(id: "answer", text: "Done", partial: false),
            .event(AgentEvent(
                sessionID: session,
                seq: 42,
                at: Date(timeIntervalSince1970: 0),
                backend: .codex,
                nativeType: nil,
                kind: .note(NoteEvent(
                    text: NoteEvent.autoshipPrefix + " merged into main"))))
        ]

        let revision = TranscriptRenderRevision(
            session: session, transcriptVersion: 7, renderedItems: items)

        #expect(revision.tailItemID == "seq-42")
    }

    @Test func streamingTheSameTailStillAdvancesItsRenderRevision() {
        let items: [TranscriptItem] = [
            .assistant(id: "answer", text: "partial", partial: true)
        ]

        let first = TranscriptRenderRevision(
            session: nil, transcriptVersion: 10, renderedItems: items)
        let next = TranscriptRenderRevision(
            session: nil, transcriptVersion: 11, renderedItems: items)

        #expect(first.tailItemID == next.tailItemID)
        #expect(first != next)
    }

    @Test func aSmallTranscriptIsOneExactBatch() {
        var segmenter = TranscriptRenderSegmenter()
        let segments = segmenter.update(rowCount: TranscriptRenderSegmenter.rowsPerSegment - 1)

        #expect(segments.map(\.range) == [0..<(TranscriptRenderSegmenter.rowsPerSegment - 1)])
    }

    @Test func renderBatchesUseTwoRows() {
        #expect(TranscriptRenderSegmenter.rowsPerSegment == 2)

        var segmenter = TranscriptRenderSegmenter()
        let segments = segmenter.update(rowCount: 5)
        #expect(segments.map(\.length) == [2, 2, 1])
    }

    @Test func hydrationStartsAtTheNewestChunkAndWalksUpward() {
        var segmenter = TranscriptRenderSegmenter()
        let chronological = segmenter.update(rowCount: 14)
        let hydration = TranscriptRenderSegmenter.hydrationOrder(chronological)

        #expect(hydration.map(\.range) == [
            12..<14, 10..<12, 8..<10, 6..<8, 4..<6, 2..<4, 0..<2,
        ])
    }

    @Test func progressiveHydrationEventuallyIncludesEveryChunk() {
        var segmenter = TranscriptRenderSegmenter()
        let chronological = segmenter.update(
            rowCount: TranscriptRenderSegmenter.rowsPerSegment * 5 + 2)
        let hydration = TranscriptRenderSegmenter.hydrationOrder(chronological)

        #expect(hydration.map(\.range) == [
            10..<12, 8..<10, 6..<8, 4..<6, 2..<4, 0..<2,
        ])
    }

    @Test func coldHeightEstimateTracksLongProseBeforeItMounts() {
        let text = Array(repeating: "A long transcript line with enough words to wrap.", count: 180)
            .joined(separator: " ")
        let items: [TranscriptItem] = [
            .assistant(id: "long", text: text, partial: false)
        ]
        let estimate = TranscriptRenderSegmentEstimate.measure(items[...])

        let wide = estimate.height(contentWidth: 720)
        let narrow = estimate.height(contentWidth: 360)
        #expect(wide > 1_000)
        #expect(narrow > wide * 1.7)
    }

    @Test func projectionWorkerReplacesATailPreviewWithCompleteHistory() async throws {
        let session = SessionID(rawValue: "projection-preview")
        let full = (1...11).map { seq in
            AgentEvent(
                sessionID: session,
                seq: UInt64(seq),
                at: Date(timeIntervalSince1970: TimeInterval(seq)),
                backend: .codex,
                nativeType: nil,
                kind: .note(NoteEvent(text: "event \(seq)")))
        }
        let worker = TranscriptProjectionWorker()

        let previewResult = await worker.update(
            events: Array(full.suffix(3)), worktreeRoot: nil,
            showDebug: true, showLock: true)
        let completeResult = await worker.update(
            events: full, worktreeRoot: nil,
            showDebug: true, showLock: true)
        let preview = try #require(previewResult)
        let complete = try #require(completeResult)
        let expected = TranscriptProjection.items(full, worktreeRoot: nil)

        #expect(preview.items.map(\.id) == ["seq-9", "seq-10", "seq-11"])
        #expect(complete.items == expected.items)
        #expect(complete.lockLines == expected.lockLines)
        #expect(complete.segments.flatMap { Array($0.range) } == Array(full.indices))
    }

    @Test func projectionWorkerDropsASupersededQueuedRevision() async {
        let worker = TranscriptProjectionWorker()
        let superseded = Task {
            await Task.yield()
            return await worker.update(
                events: [], worktreeRoot: nil, showDebug: true, showLock: true)
        }
        superseded.cancel()

        #expect(await superseded.value == nil)
    }

    @Test func segmentsAreContiguousAndBounded() {
        var segmenter = TranscriptRenderSegmenter()
        let segments = segmenter.update(rowCount: 1_003)

        #expect(segments.first?.range.lowerBound == 0)
        #expect(segments.last?.range.upperBound == 1_003)
        #expect(segments.allSatisfy { !$0.range.isEmpty })
        #expect(segments.allSatisfy {
            $0.range.count <= TranscriptRenderSegmenter.rowsPerSegment
        })
        for pair in zip(segments, segments.dropFirst()) {
            #expect(pair.0.range.upperBound == pair.1.range.lowerBound)
        }
    }

    @Test func appendOnlyTouchesTheLiveSegment() {
        var segmenter = TranscriptRenderSegmenter()
        let initialCount = TranscriptRenderSegmenter.rowsPerSegment * 5 + 1
        let grownCount = initialCount + 1
        let initial = segmenter.update(rowCount: initialCount)
        let grown = segmenter.update(rowCount: grownCount)

        #expect(Array(grown.dropLast()) == Array(initial.dropLast()))
        #expect(grown.last?.id == initial.last?.id)
        #expect(grown.last?.range == (TranscriptRenderSegmenter.rowsPerSegment * 5)..<grownCount)
    }

    @Test func crossingABoundaryPreservesSettledSegmentIdentity() {
        var segmenter = TranscriptRenderSegmenter()
        let initial = segmenter.update(rowCount: TranscriptRenderSegmenter.rowsPerSegment)
        let grown = segmenter.update(rowCount: TranscriptRenderSegmenter.rowsPerSegment + 1)

        #expect(grown.first == initial.first)
        #expect(grown.count == 2)
        #expect(grown.last?.id == TranscriptRenderSegmenter.rowsPerSegment)
    }

    @Test func shrinkRebuildsTheRangeTableExactly() {
        var segmenter = TranscriptRenderSegmenter()
        _ = segmenter.update(rowCount: 100)
        let tailCount = TranscriptRenderSegmenter.rowsPerSegment + 1
        let shrunk = segmenter.update(rowCount: tailCount)

        #expect(shrunk.map(\.range) == [
            0..<TranscriptRenderSegmenter.rowsPerSegment,
            TranscriptRenderSegmenter.rowsPerSegment..<tailCount,
        ])
        #expect(segmenter.rowCount == tailCount)
    }
}
