import APIClient
import Foundation
import Testing
import UIKit
import WireProtocol
@testable import WebTerm

/// T-iOS-28 · 会话缩略图（offscreen SwiftTerm）。
///
/// 覆盖面（Steps 测试先行）：
/// - 缓存键 (sessionId, lastOutputAt)：未变化的会话绝不重渲染；lastOutputAt
///   变化 → 新键重渲染；nil（P1 前旧服务器）→ 渲染一次后永久命中（无失效
///   信号时宁可陈旧也不无界重渲染，文档化降级）；
/// - 并发上限调度器：离屏渲染受具名常量上限（2）约束，列表滚动绝不无界
///   spawn 终端；排队 FIFO、permit 转移；全程零真实等待（continuation 屏障，
///   同 FakeClock.waitForSleepers 手法）；
/// - 404/错误/校验失败 → placeholder（且同键缓存失败结果，防滚动打爆故障主机）；
/// - 预览数据是不可信字节：id 不匹配 / data 超限 / 几何出 resizeRange →
///   placeholder 且 renderer 不被调用（data 只喂 SwiftTerm，永不字符串处理）；
/// - 真像素冒烟（仅模拟器）：小 ANSI 样例 → 非 nil、非单色快照。
@MainActor
@Suite("SessionThumbnail")
struct SessionThumbnailTests {
    // MARK: - Fixtures

    private static func makeEndpoint() -> HostEndpoint {
        guard let url = URL(string: "http://192.168.1.20:3000"),
              let endpoint = HostEndpoint(baseURL: url) else {
            fatalError("fixture endpoint must be valid")
        }
        return endpoint
    }

    private static func makeRequest(
        sessionId: UUID = UUID(), lastOutputAt: Int? = 1_000
    ) -> SessionThumbnailRequest {
        SessionThumbnailRequest(
            endpoint: makeEndpoint(), sessionId: sessionId, lastOutputAt: lastOutputAt
        )
    }

    private static func makePreview(
        for request: SessionThumbnailRequest, cols: Int = 80, rows: Int = 24,
        data: String = "hello"
    ) -> SessionPreview {
        SessionPreview(id: request.sessionId, cols: cols, rows: rows, data: data)
    }

    /// 主 actor 上的调用计数器（loader/renderer 侧记录）。
    @MainActor
    private final class Recorder {
        private(set) var loaderCalls: [SessionThumbnailRequest] = []
        private(set) var rendererCalls: [(data: String, cols: Int, rows: Int)] = []
        func recordLoad(_ request: SessionThumbnailRequest) {
            loaderCalls = loaderCalls + [request]
        }
        func recordRender(data: String, cols: Int, rows: Int) {
            rendererCalls = rendererCalls + [(data, cols, rows)]
        }
    }

    /// 可控放行的 loader 闸（测试决定何时放行；open 后一路直通）。
    /// 屏障用 continuation，不轮询不真睡（与 FakeClock.waitForSleepers 同手法）。
    @MainActor
    private final class LoaderHold {
        private var held: [CheckedContinuation<Void, Never>] = []
        private var barriers: [(target: Int, cont: CheckedContinuation<Void, Never>)] = []
        private var isOpen = false
        var heldCount: Int { held.count }

        func pass() async {
            if isOpen { return }
            await withCheckedContinuation { cont in
                held = held + [cont]
                notifyBarriers()
            }
        }

        /// 挂起直到至少 target 个 loader 停在闸上。
        func waitUntilHeld(count target: Int) async {
            if held.count >= target { return }
            await withCheckedContinuation { cont in
                barriers = barriers + [(target, cont)]
            }
        }

        /// 放行当前所有被扣住的 loader，并让后续 loader 直通。
        func open() {
            isOpen = true
            let releasing = held
            held = []
            for cont in releasing { cont.resume() }
        }

        private func notifyBarriers() {
            let met = barriers.filter { $0.target <= held.count }
            barriers = barriers.filter { $0.target > held.count }
            for barrier in met { barrier.cont.resume() }
        }
    }

    private static func makePipeline(
        recorder: Recorder,
        gate: SessionThumbnailRenderGate = SessionThumbnailRenderGate(
            limit: SessionThumbnailTunable.maxConcurrentRenders
        ),
        maxCacheEntries: Int = SessionThumbnailTunable.maxCacheEntries,
        hold: LoaderHold? = nil,
        loaderError: (any Error)? = nil,
        preview: ((SessionThumbnailRequest) -> SessionPreview)? = nil,
        rendered: UIImage? = UIImage()
    ) -> SessionThumbnailPipeline {
        SessionThumbnailPipeline(
            loader: { request in
                if let hold { await hold.pass() }
                recorder.recordLoad(request)
                if let loaderError { throw loaderError }
                return preview?(request) ?? Self.makePreview(for: request)
            },
            renderer: { data, cols, rows in
                recorder.recordRender(data: data, cols: cols, rows: rows)
                return rendered
            },
            gate: gate,
            maxCacheEntries: maxCacheEntries
        )
    }

    // MARK: - 缓存键 (sessionId, lastOutputAt)

    @Test("同键第二次请求命中缓存——loader 只调一次，返回同一图像")
    func sameKeyHitsCache() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder)
        let request = Self.makeRequest()

        let first = await pipeline.thumbnail(for: request)
        let second = await pipeline.thumbnail(for: request)

        #expect(recorder.loaderCalls.count == 1)
        #expect(first == second)
        #expect(!first.isPlaceholder)
    }

    @Test("lastOutputAt 变化（会话有新输出）→ 新键重新渲染")
    func newerLastOutputAtRerenders() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder)
        let sessionId = UUID()

        _ = await pipeline.thumbnail(for: Self.makeRequest(sessionId: sessionId, lastOutputAt: 1_000))
        _ = await pipeline.thumbnail(for: Self.makeRequest(sessionId: sessionId, lastOutputAt: 2_000))

        #expect(recorder.loaderCalls.count == 2)
    }

    @Test("lastOutputAt 为 nil（P1 前服务器）→ 渲染一次后命中缓存（文档化降级）")
    func nilLastOutputAtCachesOnce() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder)
        let request = Self.makeRequest(lastOutputAt: nil)

        _ = await pipeline.thumbnail(for: request)
        _ = await pipeline.thumbnail(for: request)

        #expect(recorder.loaderCalls.count == 1)
    }

    @Test("并发去重：同键两个并发请求 → loader 一次、两个调用者拿到同一结果")
    func concurrentSameKeyDeduplicates() async {
        let recorder = Recorder()
        let hold = LoaderHold()
        let pipeline = Self.makePipeline(recorder: recorder, hold: hold)
        let request = Self.makeRequest()

        let taskA = Task { await pipeline.thumbnail(for: request) }
        let taskB = Task { await pipeline.thumbnail(for: request) }
        await hold.waitUntilHeld(count: 1) // 只有一个 loader 在飞
        hold.open()
        let a = await taskA.value
        let b = await taskB.value

        #expect(recorder.loaderCalls.count == 1)
        #expect(a == b)
    }

    // MARK: - 并发上限调度器（滚动不得无界 spawn 离屏终端）

    @Test("5 个不同键并发：同时在渲染管线内的至多 maxConcurrentRenders(2)，其余排队")
    func concurrencyCapHoldsUnderBurst() async {
        let recorder = Recorder()
        let gate = SessionThumbnailRenderGate(limit: SessionThumbnailTunable.maxConcurrentRenders)
        let hold = LoaderHold()
        let pipeline = Self.makePipeline(recorder: recorder, gate: gate, hold: hold)
        let requests = (0..<5).map { _ in Self.makeRequest() }

        let tasks = requests.map { request in
            Task { await pipeline.thumbnail(for: request) }
        }
        await hold.waitUntilHeld(count: 2)       // 两个已过闸进入 loader
        await gate.waitUntilWaiting(count: 3)    // 其余三个停在渲染闸上

        #expect(gate.activeCount == 2)
        #expect(gate.peakActiveCount == 2)
        #expect(hold.heldCount == 2)

        hold.open()
        for task in tasks { _ = await task.value }

        #expect(gate.peakActiveCount == SessionThumbnailTunable.maxConcurrentRenders)
        #expect(recorder.loaderCalls.count == 5)
        #expect(gate.activeCount == 0)
    }

    @Test("gate FIFO：permit 按排队顺序转移")
    func gateIsFifo() async {
        let gate = SessionThumbnailRenderGate(limit: 1)
        await gate.acquire()
        let order = OrderBox()

        let third = Task {
            await gate.acquire()
            order.append(3)
            gate.release()
        }
        await gate.waitUntilWaiting(count: 1)
        let fourth = Task {
            await gate.acquire()
            order.append(4)
            gate.release()
        }
        await gate.waitUntilWaiting(count: 2)

        gate.release() // permit 依次传给 3 再 4
        _ = await third.value
        _ = await fourth.value

        #expect(order.values == [3, 4])
        #expect(gate.activeCount == 0)
    }

    @MainActor
    private final class OrderBox {
        private(set) var values: [Int] = []
        func append(_ value: Int) { values = values + [value] }
    }

    // MARK: - placeholder 路径（404/错误/校验失败）

    @Test("loader 抛错（如 404 sessionNotFound）→ placeholder，且失败同键缓存不重试")
    func loaderFailureYieldsCachedPlaceholder() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(
            recorder: recorder, loaderError: APIClientError.sessionNotFound
        )
        let request = Self.makeRequest()

        let first = await pipeline.thumbnail(for: request)
        let second = await pipeline.thumbnail(for: request)

        #expect(first.isPlaceholder)
        #expect(second.isPlaceholder)
        #expect(recorder.loaderCalls.count == 1) // 失败也按键缓存
        #expect(recorder.rendererCalls.isEmpty)
    }

    @Test("renderer 返回 nil（快照失败）→ placeholder")
    func rendererNilYieldsPlaceholder() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder, rendered: nil)

        let result = await pipeline.thumbnail(for: Self.makeRequest())

        #expect(result.isPlaceholder)
        #expect(recorder.rendererCalls.count == 1)
    }

    // MARK: - 不可信服务器数据（renderer 不被调用即为「拒于边界」）

    @Test("preview.id 与请求的 sessionId 不匹配 → placeholder，renderer 不调")
    func mismatchedPreviewIdRejected() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder, preview: { _ in
            SessionPreview(id: UUID(), cols: 80, rows: 24, data: "x") // id ≠ request.sessionId
        })

        let result = await pipeline.thumbnail(for: Self.makeRequest())

        #expect(result.isPlaceholder)
        #expect(recorder.rendererCalls.isEmpty)
    }

    @Test("data 超出 maxPreviewDataBytes → placeholder，renderer 不调")
    func oversizedPreviewDataRejected() async {
        let recorder = Recorder()
        let oversized = String(
            repeating: "a", count: SessionThumbnailTunable.maxPreviewDataBytes + 1
        )
        let pipeline = Self.makePipeline(recorder: recorder, preview: { request in
            Self.makePreview(for: request, data: oversized)
        })

        let result = await pipeline.thumbnail(for: Self.makeRequest())

        #expect(result.isPlaceholder)
        #expect(recorder.rendererCalls.isEmpty)
    }

    @Test("几何出服务器 resizeRange（0 / 1001）→ placeholder，renderer 不调", arguments: [
        (0, 24), (80, 0), (1_001, 24), (80, 1_001),
    ])
    func outOfRangeGeometryRejected(cols: Int, rows: Int) async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder, preview: { request in
            Self.makePreview(for: request, cols: cols, rows: rows)
        })

        let result = await pipeline.thumbnail(for: Self.makeRequest())

        #expect(result.isPlaceholder)
        #expect(recorder.rendererCalls.isEmpty)
    }

    @Test("几何钳制：合法值透传；1→2（镜像 web max(2,·)）；大值钳到缩略图上限")
    func geometryClamping() {
        let normal = SessionThumbnailPipeline.clampedGeometry(cols: 80, rows: 24)
        #expect(normal?.cols == 80 && normal?.rows == 24)

        let tiny = SessionThumbnailPipeline.clampedGeometry(cols: 1, rows: 1)
        #expect(tiny?.cols == SessionThumbnailTunable.minRenderGrid)
        #expect(tiny?.rows == SessionThumbnailTunable.minRenderGrid)

        let huge = SessionThumbnailPipeline.clampedGeometry(cols: 1_000, rows: 1_000)
        #expect(huge?.cols == SessionThumbnailTunable.maxRenderCols)
        #expect(huge?.rows == SessionThumbnailTunable.maxRenderRows)

        #expect(SessionThumbnailPipeline.clampedGeometry(cols: 0, rows: 24) == nil)
        #expect(SessionThumbnailPipeline.clampedGeometry(cols: 80, rows: 1_001) == nil)
    }

    @Test("渲染几何真被钳制后才交给 renderer")
    func rendererReceivesClampedGeometry() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder, preview: { request in
            Self.makePreview(for: request, cols: 1_000, rows: 1)
        })

        _ = await pipeline.thumbnail(for: Self.makeRequest())

        #expect(recorder.rendererCalls.count == 1)
        #expect(recorder.rendererCalls.first?.cols == SessionThumbnailTunable.maxRenderCols)
        #expect(recorder.rendererCalls.first?.rows == SessionThumbnailTunable.minRenderGrid)
    }

    // MARK: - LRU 缓存

    @Test("LRU：容量 3 时命中会前移——插 A,B,C、命中 A、插 D → 逐出 B，A/C/D 仍在")
    func lruEvictsLeastRecentlyUsed() async {
        let recorder = Recorder()
        let pipeline = Self.makePipeline(recorder: recorder, maxCacheEntries: 3)
        let a = Self.makeRequest()
        let b = Self.makeRequest()
        let c = Self.makeRequest()
        let d = Self.makeRequest()

        _ = await pipeline.thumbnail(for: a)
        _ = await pipeline.thumbnail(for: b)
        _ = await pipeline.thumbnail(for: c)
        _ = await pipeline.thumbnail(for: a) // bump A → LRU 现在是 B
        _ = await pipeline.thumbnail(for: d) // 逐出 B
        #expect(recorder.loaderCalls.count == 4)

        _ = await pipeline.thumbnail(for: b) // B 已被逐出 → 重新加载
        #expect(recorder.loaderCalls.count == 5)

        _ = await pipeline.thumbnail(for: a) // C 被 B 的回插逐出也无妨——A 仍应在
        #expect(recorder.loaderCalls.count == 5)
    }

    // MARK: - 常量与文案

    @Test("具名常量在位：并发上限 2、缓存上限 > 0、数据上限覆盖服务器默认 24KB")
    func tunablesArePinned() {
        #expect(SessionThumbnailTunable.maxConcurrentRenders == 2)
        #expect(SessionThumbnailTunable.maxCacheEntries > 0)
        // 服务器默认 previewBytes = 24*1024（src/config.ts:46，env 可调）——
        // 客户端防御上限必须至少覆盖默认值。
        #expect(SessionThumbnailTunable.maxPreviewDataBytes >= 24 * 1024)
        #expect(SessionThumbnailTunable.minRenderGrid == 2)
    }

    @Test("用户可见文案：中文具名常量、非空")
    func copyIsNonEmptyChinese() {
        #expect(!SessionThumbnailCopy.thumbnailLabel.isEmpty)
    }

    // MARK: - 像素冒烟（仅模拟器；真 SwiftTerm 离屏渲染）

    #if targetEnvironment(simulator)
    @Test("离屏渲染小 ANSI 样例 → 非 nil、宽>高、非单色、宽度受快照预算约束")
    func rendererSmokeProducesNonBlankImage() throws {
        let sample = "\u{1b}[31mRED\u{1b}[0m plain \u{1b}[44m BLUE-BG \u{1b}[0m"
        let image = SessionThumbnailRenderer.render(data: sample, cols: 20, rows: 5)

        let snapshot = try #require(image)
        #expect(snapshot.size.width > 0 && snapshot.size.height > 0)
        #expect(snapshot.size.width > snapshot.size.height) // 20×5 网格是横幅
        #expect(snapshot.size.width <= SessionThumbnailRenderer.snapshotTargetWidth + 1)
        #expect(Self.hasMultipleColors(snapshot)) // 空白/纯色 = 渲染管线断了
    }

    /// 把快照解到 RGBA 缓冲里扫描：出现 ≥2 种像素值即「画上东西了」。
    private static func hasMultipleColors(_ image: UIImage) -> Bool {
        guard let cgImage = image.cgImage else { return false }
        let width = min(cgImage.width, 64)
        let height = min(cgImage.height, 64)
        let bytesPerPixel = 4
        var buffer = [UInt8](repeating: 0, count: width * height * bytesPerPixel)
        guard let context = CGContext(
            data: &buffer, width: width, height: height, bitsPerComponent: 8,
            bytesPerRow: width * bytesPerPixel,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
        ) else { return false }
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
        let first = Array(buffer.prefix(bytesPerPixel))
        for pixel in stride(from: 0, to: buffer.count, by: bytesPerPixel)
        where Array(buffer[pixel..<(pixel + bytesPerPixel)]) != first {
            return true
        }
        return false
    }
    #endif
}
