import Foundation
import SessionCore
import TestSupport
import Testing
import WireProtocol
@testable import WebTerm

/// T-iOS-25 · Quick-reply chips + 常用语面板。
///
/// 覆盖面（Steps 测试先行）：
/// - 内置 chips 逐项镜像 web `public/quick-reply.ts` `BUILT_IN_CHIPS`
///   （id/text/label/appendEnter 与顺序；Esc 字节经 `KeyByteMap` 解析——
///   App 层禁止手写转义序列）；
/// - chip 点击 → `input` 帧（文本 + `\r`，Enter 是 0x0D 不是 0x0A），
///   复用 TerminalViewModel 的有序发送泵：快速连点 = 按序两帧、绝不交错；
/// - 自定义面板 CRUD + 重排：纯不可变操作（镜像 add/remove/reorder/update
///   Chip），UserDefaults 持久化（可注入 suite；非机密，不进 Keychain），
///   越界重排返回原样、损坏/异形存档 → 逐项过滤而非 crash（存储边界不可信）；
/// - 浮出时机：仅 waiting（SessionEvent 流上的持牌 gate——status waiting
///   pending=true 的流内投影）才显示；gate 解除、exited/failed（read-only）
///   → 隐藏。
@MainActor
@Suite("QuickReply")
struct QuickReplyTests {
    // MARK: - Fixtures

    private enum ServerFrames {
        static func attached(_ id: UUID) -> String {
            #"{"type":"attached","sessionId":"\#(id.uuidString.lowercased())"}"#
        }

        static func status(pending: Bool, gate: String? = nil) -> String {
            var fields = ["\"type\":\"status\"", "\"status\":\"waiting\"", "\"pending\":\(pending)"]
            if let gate { fields.append("\"gate\":\"\(gate)\"") }
            return "{\(fields.joined(separator: ","))}"
        }

        static func exit(code: Int) -> String {
            #"{"type":"exit","code":\#(code)}"#
        }
    }

    private static func chip(
        id: String = "user_t", text: String = "yes", label: String = "yes",
        appendEnter: Bool = true
    ) -> QuickReplyChip {
        QuickReplyChip(id: id, text: text, label: label, appendEnter: appendEnter)
    }

    /// 每测试独立的 UserDefaults suite（模式同 LastSessionStoreTests）。
    private static func makeSuiteName() -> String {
        "quick-reply-tests-\(UUID().uuidString)"
    }

    @MainActor
    private final class NoopHaptics: HapticSignaling {
        func gateDidArrive() {}
    }

    // MARK: - Harness（真 engine + 生产同款 fan-out 双分支接线）

    @MainActor
    private final class Harness {
        let transport = FakeTransport()
        let clock = FakeClock()
        let engine: SessionEngine
        let fanOut: EventFanOut<SessionEvent>
        let terminalViewModel: TerminalViewModel
        let gateViewModel: GateViewModel

        init() throws {
            let baseURL = try #require(URL(string: "http://192.168.1.5:3000"))
            let endpoint = try #require(HostEndpoint(baseURL: baseURL))
            engine = SessionEngine(
                transport: transport, clock: clock, endpoint: endpoint,
                eventsSource: { _ in [] }
            )
            fanOut = EventFanOut(source: engine.events, branchCount: 2)
            terminalViewModel = TerminalViewModel(engine: engine, events: fanOut.branches[0])
            gateViewModel = GateViewModel(
                engine: engine, events: fanOut.branches[1],
                haptics: NoopHaptics(), clock: clock
            )
            terminalViewModel.start()
            gateViewModel.start()
        }

        /// 标准前奏：open → .connecting → .connected → attached（3 事件到两个 VM）。
        func openAndAdopt(_ id: UUID = UUID()) async {
            await engine.open(sessionId: nil, cwd: nil)
            await transport.emit(frame: ServerFrames.attached(id))
            await waitBothProcessed(eventCount: 3)
        }

        /// 两条 fan-out 分支各自达到事件水位（可见性由两个 VM 共同驱动）。
        func waitBothProcessed(eventCount: Int) async {
            await terminalViewModel.waitUntilProcessed(eventCount: eventCount)
            await gateViewModel.waitUntilProcessed(eventCount: eventCount)
        }

        var isBarVisible: Bool {
            QuickReplyBar.isVisible(
                gate: gateViewModel.currentGate,
                isReadOnly: terminalViewModel.isReadOnly
            )
        }

        /// 连接 0 上客户端发出的帧（attach 先行，随后 input）。
        func wireFrames() async -> [String] {
            let byConnection = await transport.sentFramesByConnection
            return byConnection.first ?? []
        }
    }

    // MARK: - 内置 chips（镜像 public/quick-reply.ts BUILT_IN_CHIPS）

    @Test("内置 chips 逐项镜像 web：id/text/label/appendEnter 与顺序完全一致")
    func builtInChipsMirrorWebDefaults() {
        let chips = QuickReplyPalette.builtInChips

        #expect(chips.map(\.id) == ["__yes", "__continue", "__1", "__2", "__3", "__esc"])
        #expect(chips.map(\.text) == [
            "yes", "continue", "1", "2", "3", KeyByteMap.bytes(for: .esc),
        ])
        #expect(chips.map(\.label) == ["yes", "continue", "1", "2", "3", "Esc"])
        #expect(chips.map(\.appendEnter) == [true, true, true, true, true, false])
    }

    @Test("payload：appendEnter → 文本 + Enter（\\r，0x0D——CLAUDE.md gotcha）；否则原文")
    func payloadComposition() {
        let enter = Self.chip(text: "continue", appendEnter: true)
        #expect(QuickReplyPalette.payload(for: enter)
            == "continue" + KeyByteMap.bytes(for: .enter))
        #expect(QuickReplyPalette.payload(for: enter) == "continue\r")

        let bare = Self.chip(text: KeyByteMap.bytes(for: .esc), appendEnter: false)
        #expect(QuickReplyPalette.payload(for: bare) == KeyByteMap.bytes(for: .esc))
    }

    // MARK: - 纯不可变 CRUD（镜像 addChip/removeChip/reorderChip/updateChip）

    @Test("adding 追加到末尾；removing 按 id 过滤；未知 id 原样")
    func addingAndRemoving() {
        let a = Self.chip(id: "user_a")
        let b = Self.chip(id: "user_b")

        let added = QuickReplyPalette.adding([a], b)
        #expect(added == [a, b])

        #expect(QuickReplyPalette.removing(added, id: "user_a") == [b])
        #expect(QuickReplyPalette.removing(added, id: "user_zzz") == [a, b])
    }

    @Test("reordering：前移/后移语义同 web splice；任一索引越界 → 原样返回")
    func reorderingMovesAndBoundsChecks() {
        let a = Self.chip(id: "user_a")
        let b = Self.chip(id: "user_b")
        let c = Self.chip(id: "user_c")
        let chips = [a, b, c]

        #expect(QuickReplyPalette.reordering(chips, fromIndex: 0, toIndex: 2) == [b, c, a])
        #expect(QuickReplyPalette.reordering(chips, fromIndex: 2, toIndex: 0) == [c, a, b])
        #expect(QuickReplyPalette.reordering(chips, fromIndex: 3, toIndex: 0) == chips)
        #expect(QuickReplyPalette.reordering(chips, fromIndex: 0, toIndex: -1) == chips)
    }

    @Test("updating：按 id 浅合并 patch，id 不可变，其余 chip 不受影响；未知 id 原样")
    func updatingPatchesMatchingChipOnly() {
        let a = Self.chip(id: "user_a", text: "yes", label: "yes", appendEnter: true)
        let b = Self.chip(id: "user_b", text: "go", label: "Go", appendEnter: false)

        let updated = QuickReplyPalette.updating(
            [a, b], id: "user_a", text: "no", label: "No"
        )
        #expect(updated == [
            QuickReplyChip(id: "user_a", text: "no", label: "No", appendEnter: true),
            b,
        ])

        #expect(QuickReplyPalette.updating([a, b], id: "user_zzz", text: "x") == [a, b])
    }

    // MARK: - Store：UserDefaults 持久化（可注入 suite）

    @Test("addChip：修剪空白、label 空则取 text、id 带 user_ 前缀且唯一；跨实例持久")
    func addPersistsAcrossInstances() throws {
        let suiteName = Self.makeSuiteName()
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let store = QuickReplyStore(defaults: defaults)
        #expect(store.addChip(text: "  跑测试 ", label: "  ", appendEnter: true))
        #expect(store.addChip(text: "git status", label: "状态", appendEnter: false))

        let chips = store.userChips
        #expect(chips.map(\.text) == ["跑测试", "git status"])
        #expect(chips.map(\.label) == ["跑测试", "状态"])
        #expect(chips.map(\.appendEnter) == [true, false])
        #expect(chips.allSatisfy { $0.id.hasPrefix(QuickReplyPalette.userChipIdPrefix) })
        #expect(chips[0].id != chips[1].id)

        // 全部 chips = 内置在前 + 自定义在后（web render 顺序）。
        #expect(store.allChips == QuickReplyPalette.builtInChips + chips)

        // 新实例（同 suite）读回同一面板。
        let reloaded = QuickReplyStore(defaults: defaults)
        #expect(reloaded.userChips == chips)
    }

    @Test("addChip：空文本（含纯空白）是 no-op，返回 false 且不持久化")
    func emptyTextAddIsNoOp() throws {
        let suiteName = Self.makeSuiteName()
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let store = QuickReplyStore(defaults: defaults)
        #expect(!store.addChip(text: "   ", label: "标签", appendEnter: true))
        #expect(store.userChips.isEmpty)
        #expect(QuickReplyStore(defaults: defaults).userChips.isEmpty)
    }

    @Test("remove/update/reorder 均持久化：新实例读回操作后的面板")
    func mutationsPersistAcrossInstances() throws {
        let suiteName = Self.makeSuiteName()
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let store = QuickReplyStore(defaults: defaults)
        #expect(store.addChip(text: "a", label: "A", appendEnter: true))
        #expect(store.addChip(text: "b", label: "B", appendEnter: true))
        #expect(store.addChip(text: "c", label: "C", appendEnter: true))
        let ids = store.userChips.map(\.id)

        // reorder: A,B,C → B,C,A；update B 的 label；remove C。
        store.moveChip(fromIndex: 0, toIndex: 2)
        store.updateChip(id: ids[1], label: "B改")
        store.removeChip(id: ids[2])

        let expected = [
            QuickReplyChip(id: ids[1], text: "b", label: "B改", appendEnter: true),
            QuickReplyChip(id: ids[0], text: "a", label: "A", appendEnter: true),
        ]
        #expect(store.userChips == expected)
        #expect(QuickReplyStore(defaults: defaults).userChips == expected)
    }

    @Test("SwiftUI onMove 适配（IndexSet + 目的地约定）→ 与纯 reordering 同结果")
    func onMoveAdapterMatchesReorderingSemantics() throws {
        let suiteName = Self.makeSuiteName()
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let store = QuickReplyStore(defaults: defaults)
        #expect(store.addChip(text: "a", label: "A", appendEnter: true))
        #expect(store.addChip(text: "b", label: "B", appendEnter: true))
        #expect(store.addChip(text: "c", label: "C", appendEnter: true))

        // List.onMove：把第 0 行拖到末尾 → fromOffsets {0}, toOffset 3。
        store.moveChips(fromOffsets: IndexSet(integer: 0), toOffset: 3)
        #expect(store.userChips.map(\.text) == ["b", "c", "a"])
    }

    @Test("损坏存档 → 空面板；异形数组 → 逐项过滤只留合法 chip（存储边界不可信）")
    func corruptOrPartiallyInvalidArchiveIsFiltered() throws {
        let suiteName = Self.makeSuiteName()
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        defer { defaults.removePersistentDomain(forName: suiteName) }

        // 整体损坏（非 JSON）→ []。
        defaults.set(Data("not json at all".utf8), forKey: QuickReplyStore.paletteDefaultsKey)
        #expect(QuickReplyStore(defaults: defaults).userChips.isEmpty)

        // 根不是数组 → []。
        defaults.set(Data(#"{"id":"user_a"}"#.utf8), forKey: QuickReplyStore.paletteDefaultsKey)
        #expect(QuickReplyStore(defaults: defaults).userChips.isEmpty)

        // 数组里混入垃圾条目 → 只留形状完整的（镜像 web isValidChip filter）。
        let mixed = #"""
        [
          {"id":"user_a","text":"yes","label":"yes","appendEnter":true},
          {"id":42,"text":"x","label":"x","appendEnter":true},
          "junk",
          {"id":"user_b","text":"go","label":"Go"},
          {"id":"user_c","text":"go","label":"Go","appendEnter":false}
        ]
        """#
        defaults.set(Data(mixed.utf8), forKey: QuickReplyStore.paletteDefaultsKey)
        #expect(QuickReplyStore(defaults: defaults).userChips == [
            QuickReplyChip(id: "user_a", text: "yes", label: "yes", appendEnter: true),
            QuickReplyChip(id: "user_c", text: "go", label: "Go", appendEnter: false),
        ])
    }

    // MARK: - 浮出时机（waiting = 流上的持牌 gate；read-only → 隐藏）

    @Test("无 gate → 隐藏；gate 升起（tool/plan 均可）→ 浮出；gate 解除 → 隐藏")
    func visibilityFollowsHeldGate() async throws {
        let harness = try Harness()
        await harness.openAndAdopt()
        #expect(!harness.isBarVisible)

        await harness.transport.emit(frame: ServerFrames.status(pending: true, gate: "tool"))
        await harness.waitBothProcessed(eventCount: 4)
        #expect(harness.isBarVisible)

        await harness.transport.emit(frame: ServerFrames.status(pending: false))
        await harness.waitBothProcessed(eventCount: 5)
        #expect(!harness.isBarVisible)

        await harness.transport.emit(frame: ServerFrames.status(pending: true, gate: "plan"))
        await harness.waitBothProcessed(eventCount: 6)
        #expect(harness.isBarVisible)
    }

    @Test("exited（read-only）→ 即使 gate 仍持牌也隐藏")
    func exitedTerminalHidesChipsEvenWithHeldGate() async throws {
        let harness = try Harness()
        await harness.openAndAdopt()
        await harness.transport.emit(frame: ServerFrames.status(pending: true, gate: "tool"))
        await harness.waitBothProcessed(eventCount: 4)
        #expect(harness.isBarVisible)

        await harness.transport.emit(frame: ServerFrames.exit(code: 0))
        await harness.waitBothProcessed(eventCount: 5)
        #expect(harness.terminalViewModel.isReadOnly)
        #expect(!harness.isBarVisible)
    }

    // MARK: - 发送路径（经 TerminalViewModel 有序泵 → engine → wire）

    @Test("chip 点击 → input 帧（文本+\\r）经有序泵上线")
    func chipTapSendsInputFrame() async throws {
        let harness = try Harness()
        await harness.openAndAdopt()
        let yes = QuickReplyPalette.builtInChips[0]

        QuickReplyBar.send(yes, through: harness.terminalViewModel)
        await harness.terminalViewModel.waitUntilForwarded(sendCount: 1)

        #expect(await harness.wireFrames() == [
            MessageCodec.encode(.attach(sessionId: nil, cwd: nil)),
            MessageCodec.encode(.input(data: "yes\r")),
        ])
    }

    @Test("快速连点：两帧按点击顺序上线，绝不交错（复用 VM 发送泵）")
    func rapidDoubleTapStaysOrdered() async throws {
        let harness = try Harness()
        await harness.openAndAdopt()
        let yes = QuickReplyPalette.builtInChips[0]
        let one = QuickReplyPalette.builtInChips[2]

        QuickReplyBar.send(yes, through: harness.terminalViewModel)
        QuickReplyBar.send(one, through: harness.terminalViewModel)
        await harness.terminalViewModel.waitUntilForwarded(sendCount: 2)

        #expect(await harness.wireFrames() == [
            MessageCodec.encode(.attach(sessionId: nil, cwd: nil)),
            MessageCodec.encode(.input(data: "yes\r")),
            MessageCodec.encode(.input(data: "1\r")),
        ])
    }

    @Test("exited 后点击 → VM read-only 守卫丢弃，wire 上无 input 帧")
    func tapOnExitedTerminalIsDropped() async throws {
        let harness = try Harness()
        await harness.openAndAdopt()
        await harness.transport.emit(frame: ServerFrames.exit(code: 0))
        await harness.waitBothProcessed(eventCount: 4)

        QuickReplyBar.send(QuickReplyPalette.builtInChips[0], through: harness.terminalViewModel)

        #expect(harness.terminalViewModel.droppedReadOnlyInputCount == 1)
        #expect(await harness.wireFrames() == [
            MessageCodec.encode(.attach(sessionId: nil, cwd: nil)),
        ])
    }

    // MARK: - 面板文案（中文具名常量）

    @Test("面板/编辑器文案：中文具名常量在位")
    func panelCopyConstants() {
        #expect(QuickReplyPanel.Copy.title == "常用语")
        #expect(QuickReplyPanel.Copy.addSection == "添加新常用语")
        #expect(QuickReplyPanel.Copy.customSection == "自定义常用语")
        #expect(QuickReplyPanel.Copy.textPlaceholder == "要发送的文本")
        #expect(QuickReplyPanel.Copy.labelPlaceholder == "标签（可选，默认同文本）")
        #expect(QuickReplyPanel.Copy.appendEnterToggle == "发送后自动回车")
        #expect(QuickReplyPanel.Copy.addButton == "添加")
        #expect(QuickReplyPanel.Copy.emptyState == "还没有自定义常用语")
        #expect(QuickReplyBar.Copy.managePhrases == "管理常用语")
    }
}
