import APIClient
import Foundation
import Testing
@testable import WebTerm

/// T-iOS-26 · Projects 列表的纯分组逻辑 —— 逐条镜像 web v0.6 的
/// public/projects.ts 规则（filterProjects / sortProjects / groupProjects /
/// displayLabel）：
/// - namespace = 名字的前两个点分段（不足两段 → 无 namespace）；
/// - 成员 < MIN_GROUP_SIZE(2) 的 namespace 塌进 Other；
/// - 有运行中会话的项目**复制**进置顶的 "Active now" 组（sessions[].exited
///   字段实测存在 —— active 置顶按任务要求 assert reality 后实现）；
/// - 一个 namespace 组都没有 → 单一 flat 组（无 chrome 的平铺网格回退）；
/// - 组 key 与 web 逐字节一致（" active" / " other" / namespace 原始大小写），
///   因为 collapsed 状态经 /prefs 跨端共享，key 不一致就互相丢状态。
struct ProjectGroupingTests {
    // MARK: - Fixtures

    private static func session(exited: Bool) -> ProjectSessionRef {
        ProjectSessionRef(
            id: UUID(), title: nil, status: .working,
            clientCount: 1, createdAt: 0, exited: exited
        )
    }

    private static func project(
        _ name: String,
        path: String? = nil,
        lastActiveMs: Int? = nil,
        running: Bool = false,
        exitedSession: Bool = false
    ) -> ProjectInfo {
        var sessions: [ProjectSessionRef] = []
        if running { sessions.append(session(exited: false)) }
        if exitedSession { sessions.append(session(exited: true)) }
        return ProjectInfo(
            name: name, path: path ?? "/repos/\(name)", isGit: true,
            branch: "main", dirty: nil, lastActiveMs: lastActiveMs,
            sessions: sessions
        )
    }

    // MARK: - filter（大小写不敏感的 name/path 子串，镜像 filterProjects）

    @Test("filter：空查询原样返回全部（含前后空白仅剩空白的查询）")
    func filterEmptyQueryReturnsAll() {
        let projects = [Self.project("a"), Self.project("b")]

        #expect(ProjectGrouping.filter(projects, query: "") == projects)
        #expect(ProjectGrouping.filter(projects, query: "   ") == projects)
    }

    @Test("filter：name 与 path 子串均命中，大小写不敏感")
    func filterMatchesNameAndPathCaseInsensitively() {
        let byName = Self.project("Billo.Platform.api", path: "/x/one")
        let byPath = Self.project("zzz", path: "/Users/dev/BILLO-extras")
        let miss = Self.project("other", path: "/y/two")

        let result = ProjectGrouping.filter([byName, byPath, miss], query: "billo")

        #expect(result == [byName, byPath])
    }

    // MARK: - sort（favourites 优先 → lastActiveMs 降序 → 输入序稳定）

    @Test("sort：收藏优先，其余按 lastActiveMs 降序，缺失视为 0")
    func sortFavouritesFirstThenRecency() {
        let fav = Self.project("fav", lastActiveMs: 1)
        let newer = Self.project("newer", lastActiveMs: 100)
        let older = Self.project("older", lastActiveMs: 50)
        let never = Self.project("never", lastActiveMs: nil)

        let result = ProjectGrouping.sort(
            [never, older, fav, newer], favourites: [fav.path]
        )

        #expect(result == [fav, newer, older, never])
    }

    @Test("sort：全键相等时保持输入顺序（稳定排序，镜像 JS stable sort）")
    func sortIsStableOnTies() {
        let a = Self.project("a", lastActiveMs: 5)
        let b = Self.project("b", lastActiveMs: 5)
        let c = Self.project("c", lastActiveMs: 5)

        #expect(ProjectGrouping.sort([a, b, c], favourites: []) == [a, b, c])
    }

    // MARK: - namespace 分组

    @Test("group：≥2 成员的 namespace 成组，单成员与无点名塌进 Other")
    func groupNamespacesAndOther() {
        let apiProj = Self.project("Billo.Platform.api", lastActiveMs: 2)
        let webProj = Self.project("Billo.Platform.web", lastActiveMs: 1)
        let solo = Self.project("solo.thing")
        let plain = Self.project("plain")

        let groups = ProjectGrouping.group(
            [apiProj, webProj, solo, plain], favourites: []
        )

        #expect(groups.count == 2)
        #expect(groups[0].kind == .namespace)
        #expect(groups[0].key == "Billo.Platform")
        #expect(groups[0].label == "Billo.Platform")
        #expect(groups[0].projects == [apiProj, webProj])
        #expect(groups[1].kind == .other)
        #expect(groups[1].key == ProjectGrouping.otherGroupKey)
        // 镜像 web groupProjects：other 先收无点名（noNamespace），再追加
        // 塌掉的单成员 namespace（public/projects.ts:163-166）→ plain 在前。
        #expect(groups[1].projects == [plain, solo])
    }

    @Test("group：namespace 桶大小写不敏感合并，显示名取首见大小写")
    func groupBucketsCaseInsensitively() {
        let first = Self.project("Billo.Platform.a")
        let second = Self.project("billo.platform.b")

        let groups = ProjectGrouping.group([first, second], favourites: [])

        #expect(groups.count == 1)
        #expect(groups[0].key == "Billo.Platform")
        #expect(groups[0].projects == [first, second])
    }

    @Test("group：没有任何 namespace 组 → 单一 flat 组（全部项目，无 chrome 回退）")
    func groupFlatFallback() {
        let solo = Self.project("solo.thing", lastActiveMs: 1)
        let plain = Self.project("plain", lastActiveMs: 2)

        let groups = ProjectGrouping.group([solo, plain], favourites: [])

        #expect(groups.count == 1)
        #expect(groups[0].kind == .flat)
        #expect(groups[0].key == ProjectGrouping.otherGroupKey)
        #expect(groups[0].projects == [plain, solo]) // recency 降序
        #expect(groups[0].isCollapsible == false)
    }

    @Test("group：运行中的项目复制进置顶 Active 组；activeCount 逐组统计")
    func groupActivePinning() {
        let runningA = Self.project("Billo.Platform.api", lastActiveMs: 2, running: true)
        let idleB = Self.project("Billo.Platform.web", lastActiveMs: 1)
        let exitedOnly = Self.project("Billo.Platform.cli", exitedSession: true)

        let groups = ProjectGrouping.group(
            [runningA, idleB, exitedOnly], favourites: []
        )

        #expect(groups.count == 2)
        #expect(groups[0].kind == .active)
        #expect(groups[0].key == ProjectGrouping.activeGroupKey)
        #expect(groups[0].projects == [runningA]) // exited 会话不算 running
        #expect(groups[0].activeCount == 1)
        #expect(groups[1].kind == .namespace)
        #expect(groups[1].projects.contains(runningA)) // 复制而非移动
        #expect(groups[1].activeCount == 1)
        #expect(groups[0].isCollapsible == false) // Active now 永远展开
        #expect(groups[1].isCollapsible == true)
    }

    @Test("group：namespace 组按组内最新 lastActiveMs 降序，平局按 label 升序")
    func groupOrdersNamespacesByRecencyThenLabel() {
        let older1 = Self.project("Aaa.Team.x", lastActiveMs: 10)
        let older2 = Self.project("Aaa.Team.y", lastActiveMs: 20)
        let newer1 = Self.project("Zzz.Team.x", lastActiveMs: 5)
        let newer2 = Self.project("Zzz.Team.y", lastActiveMs: 99)
        let tieA1 = Self.project("Bbb.Tie.x", lastActiveMs: 20)
        let tieA2 = Self.project("Bbb.Tie.y", lastActiveMs: 3)

        let groups = ProjectGrouping.group(
            [older1, older2, newer1, newer2, tieA1, tieA2], favourites: []
        )

        #expect(groups.map(\.key) == ["Zzz.Team", "Aaa.Team", "Bbb.Tie"])
    }

    // MARK: - displayLabel（组内卡片名去掉 namespace 前缀）

    @Test("displayLabel：namespace 组内剥前缀（大小写不敏感）；哨兵组保留全名")
    func displayLabelStripsNamespacePrefix() {
        #expect(ProjectGrouping.displayLabel(
            name: "Billo.Platform.api", groupKey: "Billo.Platform"
        ) == "api")
        #expect(ProjectGrouping.displayLabel(
            name: "billo.platform.api", groupKey: "Billo.Platform"
        ) == "api")
        #expect(ProjectGrouping.displayLabel(
            name: "unrelated", groupKey: "Billo.Platform"
        ) == "unrelated")
        #expect(ProjectGrouping.displayLabel(
            name: "Billo.Platform.api", groupKey: ProjectGrouping.activeGroupKey
        ) == "Billo.Platform.api")
        #expect(ProjectGrouping.displayLabel(
            name: "Billo.Platform.api", groupKey: ProjectGrouping.otherGroupKey
        ) == "Billo.Platform.api")
    }

    @Test("group：组内排序收藏优先（favourites 参与 sortProjects）")
    func groupSortsFavouritesFirstWithinGroup() {
        let apiProj = Self.project("Ns.Grp.api", lastActiveMs: 9)
        let webProj = Self.project("Ns.Grp.web", lastActiveMs: 1)

        let groups = ProjectGrouping.group(
            [apiProj, webProj], favourites: [webProj.path]
        )

        #expect(groups.count == 1)
        #expect(groups[0].projects == [webProj, apiProj])
    }
}
