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

/// T-iPad-2 · split 模式下 sidebar 选中态 ↔ 现有 `AppCoordinator` 路由的**等价
/// 映射**证明。分栏 UI 只是「又一个触发面」——它调用的仍是既有的
/// `open(id)` / `open(nil)` / `presentProjects()`，绝不新开一条会话生命周期：
///
/// - 选 `.session(id)` == 现有 `open(id)`：首帧 `attach(sessionId:id, cwd:nil)`；
/// - 选 `.newSession` == 现有 `open(nil)`：首帧 `attach(sessionId:nil, cwd:nil)`；
/// - 选 `.projects` == 现有 Projects 呈现（`isProjectsPresented` 置位）。
///
/// 二向绑定：终端经任意路径打开/关闭时，`selectedSidebarItem` 如实反映
/// （adopted 后 = `.session(adoptedId)`；关闭后归 nil）。切会话沿用单活 WS 不变式
/// 的 close→open（复用 `closeTerminal` 记 last-seen + `open`，**非**平行生命周期）。
///
/// 真 `SessionEngine`/`AppCoordinator` over FakeTransport/FakeHTTPTransport，
/// 零真实等待（openTask + waitUntilProcessed 双屏障，NewSessionInCwdTests 同款）。
@MainActor
@Suite("SidebarSelection (T-iPad-2)")
struct SidebarSelectionTests {
    private nonisolated static let base = "http://192.168.1.5:3000"

    // MARK: - Fixtures（NewSessionInCwdTests / SessionSwitcherTests 同款）

    private struct Fixture {
        let transport: FakeTransport
        let http: FakeHTTPTransport
        let host: HostRegistry.Host
        let unreadStore: InMemoryUnreadWatermarkStore
        let environment: AppEnvironment
    }

    private func makeFixture(suiteName: String) throws -> Fixture {
        let baseURL = try #require(URL(string: Self.base))
        let endpoint = try #require(HostEndpoint(baseURL: baseURL))
        let transport = FakeTransport()
        let http = FakeHTTPTransport()
        let unreadStore = InMemoryUnreadWatermarkStore()
        let host = HostRegistry.Host(id: UUID(), name: "书房 Mac", endpoint: endpoint)
        let defaults = try #require(UserDefaults(suiteName: suiteName))
        return Fixture(
            transport: transport,
            http: http,
            host: host,
            unreadStore: unreadStore,
            environment: AppEnvironment(
                hostStore: InMemoryHostStore(hosts: [host]),
                lastSessionStore: UserDefaultsLastSessionStore(defaults: defaults),
                http: http,
                termTransport: transport,
                probe: { _ in .failure(.timeout) },
                unreadStore: unreadStore
            )
        )
    }

    /// activeHost 上线（sidebar 触发面解析主机的唯一来源）。
    private func primeHost(_ coordinator: AppCoordinator) async {
        await coordinator.sessionList.reloadHosts()
    }

    /// attach 握手完成（服务器 adopted）：openTask 完成 ∧ 前 3 个事件已应用。
    private func adopt(
        _ controller: TerminalSessionController,
        transport: FakeTransport,
        sessionId: UUID
    ) async {
        await controller.openTask?.value
        await transport.emit(
            frame: #"{"type":"attached","sessionId":"\#(sessionId.uuidString.lowercased())"}"#
        )
        await controller.terminalViewModel.waitUntilProcessed(eventCount: 3)
    }

    /// 新 controller 的 open 已提交且连接事件已冲刷（帧必已落地）。
    private func awaitAttachSettled(_ controller: TerminalSessionController) async {
        await controller.openTask?.value
        await controller.terminalViewModel.waitUntilProcessed(eventCount: 2)
    }

    // MARK: - 1. SidebarItem → 路由等价（同一 coordinator API）

    @Test("选 .session(id) == 现有 open(id)：首帧 attach(sessionId:id, cwd:nil)，单一连接")
    func sessionItemOpensSameAsOpenId() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.sessionItem")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let sessionId = UUID()

        // Act：sidebar 选中一行 —— split 只是另一个触发面。
        coordinator.selectSidebarItem(.session(sessionId))

        // Assert：走的是同一 open → startTerminal 通道，首帧带该 id 的裸 attach。
        let controller = try #require(coordinator.terminalController)
        await awaitAttachSettled(controller)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection.count == 1) // 无平行生命周期：只有一个 engine 连接
        #expect(byConnection[0] == [MessageCodec.encode(.attach(sessionId: sessionId, cwd: nil))])
        controller.teardown()
    }

    @Test("选 .newSession == 现有 open(nil)：首帧 attach(sessionId:nil, cwd:nil)")
    func newSessionItemOpensFreshSession() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.newSession")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)

        // Act
        coordinator.selectSidebarItem(.newSession)

        // Assert
        let controller = try #require(coordinator.terminalController)
        await awaitAttachSettled(controller)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection.count == 1)
        #expect(byConnection[0] == [MessageCodec.encode(.attach(sessionId: nil, cwd: nil))])
        controller.teardown()
    }

    @Test("选 .projects == 现有 Projects 呈现：置位 isProjectsPresented，不开终端")
    func projectsItemPresentsProjects() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.projects")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)

        // Act
        coordinator.selectSidebarItem(.projects)

        // Assert：复用既有 presentProjects —— 不产生 terminalController、无连接。
        #expect(coordinator.isProjectsPresented)
        #expect(coordinator.projectsViewModel != nil)
        #expect(coordinator.terminalController == nil)
        let attempts = await fixture.transport.connectAttempts
        #expect(attempts.isEmpty)
    }

    @Test("无 activeHost → .session/.newSession 均 no-op（不 spawn、不连接）")
    func withoutActiveHostIsNoOp() async throws {
        // Arrange：不 reloadHosts —— activeHost 仍为 nil。
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.noHost")
        let coordinator = AppCoordinator(environment: fixture.environment)

        // Act
        coordinator.selectSidebarItem(.session(UUID()))
        coordinator.selectSidebarItem(.newSession)

        // Assert
        #expect(coordinator.terminalController == nil)
        let attempts = await fixture.transport.connectAttempts
        #expect(attempts.isEmpty)
    }

    // MARK: - 2. selectedSidebarItem 二向绑定（打开/关闭如实反映）

    @Test("打开终端 → selectedSidebarItem = .session(adoptedId)；closeTerminal → 归 nil")
    func selectionReflectsOpenAndClose() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.reflect")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let adopted = UUID()

        // 冷态：无选中。
        #expect(coordinator.selectedSidebarItem == nil)

        // Act：打开新会话并被服务器 adopted。
        coordinator.selectSidebarItem(.newSession)
        let controller = try #require(coordinator.terminalController)
        await adopt(controller, transport: fixture.transport, sessionId: adopted)

        // Assert：选中态如实反映 adopted 会话（detail 面板正在显示它）。
        #expect(coordinator.selectedSidebarItem == .session(adopted))

        // Act：返回列表（关闭 detail）。
        coordinator.closeTerminal()

        // Assert：无 detail → 无选中（占位「选择或新建会话」）。
        #expect(coordinator.selectedSidebarItem == nil)
    }

    @Test("killCurrentSession：有 adopted 会话 → detach detail（归 nil）；无会话 → no-op")
    func killCurrentSessionClosesDetail() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.kill")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)

        // 无会话时 kill = no-op（不崩、detail 仍空）。
        coordinator.killCurrentSession()
        #expect(coordinator.terminalController == nil)

        // Act：打开并 adopt，再 kill。
        coordinator.selectSidebarItem(.newSession)
        let controller = try #require(coordinator.terminalController)
        await adopt(controller, transport: fixture.transport, sessionId: UUID())
        coordinator.killCurrentSession()

        // Assert：detail 被 detach（kill 走既有 SessionListViewModel.kill →
        // APIClient.killSession 带 Origin，无新网络路径，由 T-iOS-8 契约测覆盖）。
        #expect(coordinator.terminalController == nil)
        #expect(coordinator.selectedSidebarItem == nil)
    }

    @Test("sidebarSelection 绑定 get/set 往返：set(.newSession) 触发打开；get 反映 adopted")
    func bindingRoundTrips() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.binding")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let adopted = UUID()
        let binding = coordinator.sidebarSelection

        // Act：经绑定 setter 选中（== NavigationSplitView 的选中回调）。
        binding.wrappedValue = .newSession
        let controller = try #require(coordinator.terminalController)
        await adopt(controller, transport: fixture.transport, sessionId: adopted)

        // Assert：getter 反映 adopted 会话。
        #expect(binding.wrappedValue == .session(adopted))
        controller.teardown()
    }

    // MARK: - 3. 切会话：close→open 复用（非平行生命周期）

    @Test("sidebar 选另一会话 → closeTerminal(记 last-seen)+open：新 controller、第二连接 attach B")
    func switchingSessionsReusesCloseThenOpen() async throws {
        // Arrange：先打开会话 A 并 adopted。
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.switch")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let sessionA = UUID()
        let sessionB = UUID()
        coordinator.selectSidebarItem(.session(sessionA))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: sessionA)

        // Act：sidebar 选中会话 B（detail 即时切换）。
        coordinator.selectSidebarItem(.session(sessionB))

        // Assert：单活 WS —— close(A)→open(B)，新 controller、第二连接 attach B。
        let second = try #require(coordinator.terminalController)
        #expect(second !== first)
        await awaitAttachSettled(second)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection.count == 2)
        #expect(byConnection[1] == [MessageCodec.encode(.attach(sessionId: sessionB, cwd: nil))])
        // 复用 closeTerminal —— A 的 last-seen 水位已记（切走即已读）。
        #expect(fixture.unreadStore.snapshot[sessionA] != nil)
        second.teardown()
    }

    @Test("重复选中当前 detail 会话 → no-op（不 close/open，无第二连接）")
    func reselectingCurrentSessionIsNoOp() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.reselect")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let sessionA = UUID()
        coordinator.selectSidebarItem(.session(sessionA))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: sessionA)

        // Act：再点同一行。
        coordinator.selectSidebarItem(.session(sessionA))

        // Assert：controller 不换、无第二连接（避免无谓 churn）。
        #expect(coordinator.terminalController === first)
        let attempts = await fixture.transport.connectAttempts
        #expect(attempts.count == 1)
        first.teardown()
    }

    // MARK: - 4. 布局切换与会话生命周期正交（T-iOS-29 .id 稳定性）

    @Test("regular→compact 切换（Slide Over）：controller 归 coordinator 所有，实例不变、选中态保持")
    func controllerIdentityStableAcrossLayoutSwitch() async throws {
        // Arrange：打开并 adopted。
        let fixture = try makeFixture(suiteName: "SidebarSelectionTests.identity")
        let coordinator = AppCoordinator(environment: fixture.environment)
        await primeHost(coordinator)
        let adopted = UUID()
        coordinator.selectSidebarItem(.newSession)
        let captured = try #require(coordinator.terminalController)
        await adopt(captured, transport: fixture.transport, sessionId: adopted)

        // Act：模拟 iPad 拉出/推回 Slide Over —— AdaptiveRootView 只换渲染的
        // 根子视图（split↔stack），会话状态归 coordinator 所有，与布局正交。
        #expect(LayoutPolicy.mode(horizontalSizeClass: .regular) == .split)
        #expect(LayoutPolicy.mode(horizontalSizeClass: .compact) == .stack)

        // Assert：controller 实例不变（复用 T-iOS-29 的 .id(controller.id) 稳定性
        // 前提：controller 不因布局分支切换而重建）、选中态如常。
        #expect(coordinator.terminalController === captured)
        #expect(coordinator.selectedSidebarItem == .session(adopted))
        captured.teardown()
    }
}
