import Foundation
import Testing
import WireProtocol
@testable import SessionCore

/// T-iOS-23 · TitleSanitizer — OSC 标题是**主机/攻击者可控输入**（plan §7）：
/// 入列表前必须净化。规则：
/// - 剥 C0/C1/DEL 控制字符（OSC 解析理应已排除 C0，但不假设 —— 一律再滤）；
/// - 剥 bidi 覆写/嵌入/隔离与零宽字符（U+200B–200F、U+202A–202E、U+2066–2069
///   —— 真正的仿冒向量；另含 U+FEFF 零宽不换行空格）；
/// - 截断到 `Tunables.titleMaxLength`（按 Character/字素簇计，emoji 不被劈开）；
/// - 首尾空白修剪（镜像 web `title.trim() || null`，public/tabs.ts:626）。
@Suite("TitleSanitizer")
struct TitleSanitizerTests {
    // MARK: - 正常路径

    @Test("普通标题原样通过（含中文与合法标点）")
    func normalTitlePassesThrough() {
        #expect(TitleSanitizer.sanitize("vim · 修改 server.ts") == "vim · 修改 server.ts")
    }

    @Test("首尾空白/换行被修剪；全空白 → 空串（调用方视为无标题）")
    func whitespaceIsTrimmed() {
        #expect(TitleSanitizer.sanitize("  claude \n") == "claude")
        #expect(TitleSanitizer.sanitize(" \t\n ").isEmpty)
        #expect(TitleSanitizer.sanitize("").isEmpty)
    }

    // MARK: - 敌意标题（任务 RED 清单）

    @Test("超长标题截断到 titleMaxLength 个字符")
    func overlongTitleIsTruncated() {
        // Arrange
        let overlong = String(repeating: "A", count: Tunables.titleMaxLength * 4)

        // Act
        let sanitized = TitleSanitizer.sanitize(overlong)

        // Assert
        #expect(sanitized.count == Tunables.titleMaxLength)
    }

    @Test("U+202E RLO 仿冒 payload：bidi 覆写被剥掉，其余字符保留")
    func rloSpoofIsStripped() {
        // Arrange: 经典 RLO 文件名仿冒 "gpj.exe" → 显示成 "exe.jpg"。
        let spoof = "evil\u{202E}gpj.exe"

        // Act & Assert
        #expect(TitleSanitizer.sanitize(spoof) == "evilgpj.exe")
    }

    @Test("零宽/双向控制全表被剥：U+200B–200F、U+202A–202E、U+2066–2069、U+FEFF")
    func zeroWidthAndBidiRangesAreStripped() {
        // Arrange: 每段区间取首尾端点 + 中间夹正常字符。
        let hostile = "a\u{200B}b\u{200F}c\u{202A}d\u{202E}e\u{2066}f\u{2069}g\u{FEFF}h"

        // Act & Assert
        #expect(TitleSanitizer.sanitize(hostile) == "abcdefgh")
    }

    @Test("C0/C1/DEL 控制字符被剥（不信任 OSC 解析已排除 —— ESC 注入零容忍）")
    func controlCharactersAreStripped() {
        // Arrange: ESC + CSI 序列、BEL、DEL、C1 CSI（U+009B）。
        let hostile = "ok\u{1B}[31mred\u{07}\u{7F}\u{9B}2Jend"

        // Act & Assert
        #expect(TitleSanitizer.sanitize(hostile) == "ok[31mred2Jend")
    }

    @Test("emoji 洪泛：按字素簇截断，不劈开 emoji、不 crash")
    func emojiFloodIsCappedByGrapheme() {
        // Arrange: 家庭 emoji（多 scalar 字素簇）× 4 倍上限。
        let flood = String(repeating: "🧑‍🧑‍🧒", count: Tunables.titleMaxLength * 4)

        // Act
        let sanitized = TitleSanitizer.sanitize(flood)

        // Assert: 上限以内（ZWJ U+200D 在剥除区间内 —— 家庭簇会拆成单人
        // emoji，安全优先于连字美观），且每个 Character 都是合法字素。
        #expect(sanitized.count <= Tunables.titleMaxLength)
        #expect(!sanitized.isEmpty)
        #expect(sanitized.allSatisfy { !$0.unicodeScalars.isEmpty })
    }

    @Test("净化幂等：sanitize(sanitize(x)) == sanitize(x)")
    func sanitizeIsIdempotent() {
        // Arrange
        let hostile = "  \u{202E}evil\u{1B}]0;fake\u{07}\u{200B} 标题  "

        // Act
        let once = TitleSanitizer.sanitize(hostile)
        let twice = TitleSanitizer.sanitize(once)

        // Assert
        #expect(once == twice)
    }
}
