import Foundation
import SwiftUI
import Testing
import WireProtocol
@testable import WebTerm

/// T-iOS-24 · Timeline sheet（完整时间线钻取）。
///
/// 覆盖面（Steps 测试先行）：
/// - `/live-sessions/:id/events` 全量渲染管线：呈现顺序**逐行镜像 web**
///   （public/timeline.ts:174-189 render()——`slice(0, maxEvents)` 先截断
///   再 reverse，newest-first 展示）；
/// - class → 图标映射逐字镜像 web `timelineIcon`（public/timeline.ts:88-96），
///   颜色为语义映射（web CSS 未定义 tl-icon-* 颜色——iOS 对齐
///   SessionListScreen 的状态色约定）；未知 class 走 fallback（防御纵深，
///   服务器是不可信输入源——即使 APIClient.decodeList 已丢弃未知类）；
/// - timeline disabled（服务器回 `[]`，src/server.ts:589-591）→ 空态而非错误；
/// - fetch 失败 → 显式可重试错误态（retry 后可达 .loaded）；
/// - digest「展开」入口的装配缝：`forSession` 工厂（sessionId 未知 → 不出
///   sheet；已知 → 原样传给 events source）。
@MainActor
@Suite("TimelineSheet")
struct TimelineSheetTests {
    // MARK: - Fixtures

    private static func event(
        at: Int, cls: String = "tool", label: String = "ran Bash"
    ) -> TimelineEvent {
        TimelineEvent(at: at, class: cls, toolName: nil, label: label)
    }

    private struct FetchError: Error {}

    /// 记录 source 收到的 sessionId（@Sendable 闭包内可变状态 → actor）。
    private actor SourceRecorder {
        private(set) var ids: [UUID] = []
        func record(_ id: UUID) { ids = ids + [id] }
    }

    // MARK: - Phase 状态机

    @Test("初始 phase = .loading（load 前不渲染内容）")
    func initialPhaseIsLoading() {
        let vm = TimelineViewModel(fetch: { [] })

        #expect(vm.phase == .loading)
    }

    @Test("load 成功：服务器 oldest-first → 展示 newest-first（镜像 web reverse）")
    func loadReversesToNewestFirst() async {
        let oldestFirst = [
            Self.event(at: 1, label: "ran Bash"),
            Self.event(at: 2, cls: "waiting", label: "waiting for approval"),
            Self.event(at: 3, cls: "done", label: "finished"),
        ]
        let vm = TimelineViewModel(fetch: { oldestFirst })

        await vm.load()

        #expect(vm.phase == .loaded(Array(oldestFirst.reversed())))
    }

    @Test("超过 maxEvents：镜像 web slice(0,50) 再 reverse——先截前 50 条再反转")
    func loadCapsThenReversesLikeWeb() async throws {
        let sixty = (1...60).map { Self.event(at: $0) }
        let vm = TimelineViewModel(fetch: { sixty })

        await vm.load()

        guard case .loaded(let shown) = vm.phase else {
            Issue.record("期望 .loaded，实际 \(vm.phase)")
            return
        }
        #expect(shown.count == TimelineViewModel.maxEvents)
        #expect(shown.first?.at == 50) // slice(0,50) 的最新一条
        #expect(shown.last?.at == 1)   // 反转后最旧的在末尾
    }

    @Test("maxEvents 钉住 web parity 值 50（public/timeline.ts:20）")
    func maxEventsMirrorsWebDefault() {
        #expect(TimelineViewModel.maxEvents == 50)
    }

    @Test("服务器回空数组（timeline disabled）→ .empty，绝不是 .failed")
    func emptyArrayIsEmptyStateNotError() async {
        let vm = TimelineViewModel(fetch: { [] })

        await vm.load()

        #expect(vm.phase == .empty)
    }

    @Test("fetch 抛错 → .failed（显式可重试错误态）")
    func fetchFailureBecomesFailedPhase() async {
        let vm = TimelineViewModel(fetch: { throw FetchError() })

        await vm.load()

        #expect(vm.phase == .failed)
    }

    @Test("重试：失败后再次 load 成功 → .loaded（错误态可恢复）")
    func retryAfterFailureRecovers() async {
        let flag = SourceRecorder() // 复用 actor 当计数器：首轮抛错，次轮成功
        let events = [Self.event(at: 7)]
        let vm = TimelineViewModel(fetch: {
            if await flag.ids.isEmpty {
                await flag.record(UUID())
                throw FetchError()
            }
            return events
        })

        await vm.load()
        #expect(vm.phase == .failed)

        await vm.load() // TimelineSheet 的「重试」按钮走的就是这条路径

        #expect(vm.phase == .loaded(events))
    }

    // MARK: - digest「展开」入口的装配缝（forSession 工厂）

    @Test("forSession(nil)：会话尚未 adopted → 不构造 VM（无 sheet 可出）")
    func forSessionWithoutIdReturnsNil() {
        let vm = TimelineViewModel.forSession(nil, source: { _ in [] })

        #expect(vm == nil)
    }

    @Test("forSession(id)：load 把 sessionId 原样传给 events source")
    func forSessionPassesSessionIdThrough() async throws {
        let expected = UUID()
        let recorder = SourceRecorder()
        let events = [Self.event(at: 42)]
        let vm = try #require(TimelineViewModel.forSession(expected, source: { id in
            await recorder.record(id)
            return events
        }))

        await vm.load()

        #expect(await recorder.ids == [expected])
        #expect(vm.phase == .loaded(events))
    }

    // MARK: - class → 图标 / 颜色映射

    @Test("图标逐字镜像 web timelineIcon（public/timeline.ts:88-96）", arguments: [
        ("tool", "🔧"),
        ("waiting", "⏳"),
        ("done", "✓"),
        ("stuck", "⚠"),
        ("user", "💬"),
    ])
    func glyphMirrorsWebTimelineIcon(cls: String, expected: String) {
        #expect(TimelineClassStyle.glyph(for: cls) == expected)
    }

    @Test("未知 class → fallback 图标（服务器不可信，映射必须全函数）")
    func unknownClassFallsBackToNeutralGlyph() {
        #expect(TimelineClassStyle.glyph(for: "future-class") == TimelineClassStyle.fallbackGlyph)
        #expect(TimelineClassStyle.glyph(for: "") == TimelineClassStyle.fallbackGlyph)
    }

    @Test("颜色语义映射：全部走冻结 DS.Palette（无裸色）；未知 → textSecondary")
    func colorMappingIsSemantic() {
        // waiting/stuck 复用状态 token（与列表同义）；done 复用 statusWorking；
        // tool/user 用 timeline 专属 token —— 无一裸色（UX 一致性 finding）。
        #expect(TimelineClassStyle.color(for: "waiting") == DS.Palette.statusWaiting)
        #expect(TimelineClassStyle.color(for: "stuck") == DS.Palette.statusStuck)
        #expect(TimelineClassStyle.color(for: "done") == DS.Palette.statusWorking)
        #expect(TimelineClassStyle.color(for: "tool") == DS.Palette.timelineTool)
        #expect(TimelineClassStyle.color(for: "user") == DS.Palette.timelineUser)
        #expect(TimelineClassStyle.color(for: "future-class") == DS.Palette.textSecondary)
    }

    // MARK: - 行时间格式（镜像 web formatHHMM：24h 墙钟 HH:mm）

    @Test("timeLabel：24h HH:mm，固定时区下确定性输出")
    func timeLabelIs24HourWallClock() throws {
        let utc = try #require(TimeZone(identifier: "UTC"))
        let onePM = (13 * 3_600 + 5 * 60) * 1_000 // 1970-01-01 13:05 UTC

        #expect(TimelineRowFormat.timeLabel(atMs: 0, timeZone: utc) == "00:00")
        #expect(TimelineRowFormat.timeLabel(atMs: onePM, timeZone: utc) == "13:05")
    }

    // MARK: - 用户可见文案（中文具名常量，空态 ≠ 错误态）

    @Test("空态与错误态文案存在、非空、且互不相同")
    func emptyAndErrorCopyAreDistinct() {
        #expect(!TimelineCopy.title.isEmpty)
        #expect(!TimelineCopy.emptyTitle.isEmpty)
        #expect(!TimelineCopy.loadFailed.isEmpty)
        #expect(!TimelineCopy.retry.isEmpty)
        #expect(TimelineCopy.emptyTitle != TimelineCopy.loadFailed)
    }
}
