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

/// T-iOS-29 · 杂项闭环：new-in-cwd + 退出会话清理。
///
/// 1. "在当前目录开新会话"（TerminalScreen 工具栏 / exit 横幅共用动作）→
///    `AppCoordinator.openNewSessionInCurrentCwd()`：单活 WS 不变式下的
///    close→open，新会话首帧 `attach(null, cwd)`（镜像 web tabs.ts
///    `newTab()` M6——取活跃会话 cwd）。cwd 解析次序：/live-sessions 行数据
///    （server-adopted id 匹配行）→ controller 的 spawnCwd（T-iOS-26 项目
///    fresh-spawn 尚未进列表轮询）→ 未知（普通新会话）。服务器数据不可信：
///    非绝对路径按未知处理（ProjectsViewModel 同款纪律）。
/// 2. exited 会话点开 → 回放渲染 + exit 横幅（TerminalViewModel P0 已有）+
///    横幅"开新会话"动作（src/session/manager.ts:145-153：exited 会话在
///    reap 前保持可列出，attach 即回放 ring buffer + 补发 exit 帧）。
///
/// 真 `SessionEngine`/`AppCoordinator` over FakeTransport/FakeHTTPTransport，
/// 零真实等待（openTask + waitUntilProcessed 双屏障，ProjectOpenWiringTests
/// 同款）。
@MainActor
@Suite("NewSessionInCwd (T-iOS-29)")
struct NewSessionInCwdTests {
    private nonisolated static let base = "http://192.168.1.5:3000"

    // MARK: - Fixtures（SessionSwitcherTests 的 WiringFixture 同款）

    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
            )
        )
    }

    private func listURL() throws -> URL {
        try #require(URL(string: "\(Self.base)/live-sessions"))
    }

    private func sessionJSON(id: UUID, cwd: String?, exited: Bool = false) -> String {
        let cwdJSON = cwd.map { ",\"cwd\":\"\($0)\"" } ?? ""
        return "{\"id\":\"\(id.uuidString.lowercased())\",\"createdAt\":1700000000000"
            + ",\"clientCount\":0,\"status\":\"idle\",\"exited\":\(exited)"
            + "\(cwdJSON),\"cols\":80,\"rows\":24}"
    }

    private func listBody(_ entries: [String]) -> Data {
        Data("[\(entries.joined(separator: ","))]".utf8)
    }

    /// 让列表 VM 先掌握行数据（cwd 的唯一服务器来源）。
    private func primeRows(_ coordinator: AppCoordinator, fixture: Fixture, rows: [String]) async throws {
        await coordinator.sessionList.reloadHosts()
        await fixture.http.queueSuccess(url: try listURL(), body: listBody(rows))
        await coordinator.sessionList.refresh()
    }

    /// attach 握手完成（服务器 adopted）：openTask 完成 ∧ 前 3 个事件
    /// （connecting/connected/adopted）已应用。
    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. 在当前会话 cwd 开新会话

    @Test("cwd 来自 /live-sessions 行 → 切换后新连接首帧 attach(null, cwd)；旧会话记 last-seen")
    func newInCwdUsesListRowCwd() async throws {
        // Arrange：列表已知该会话 cwd，打开并 adopted。
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.rowCwd")
        let coordinator = AppCoordinator(environment: fixture.environment)
        let sessionId = UUID()
        try await primeRows(
            coordinator, fixture: fixture,
            rows: [sessionJSON(id: sessionId, cwd: "/Users/dev/proj")]
        )
        coordinator.open(SessionListViewModel.OpenRequest(
            id: UUID(), host: fixture.host, sessionId: sessionId
        ))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: sessionId)

        // Act：工具栏/横幅动作。
        coordinator.openNewSessionInCurrentCwd()

        // Assert：close→open —— 新 controller、新连接，首帧带行 cwd 的 fresh spawn。
        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: nil, cwd: "/Users/dev/proj"))
        ])
        // 旧会话经 closeTerminal 记 last-seen（切走即已读）。
        #expect(fixture.unreadStore.snapshot[sessionId] != nil)
        second.teardown()
    }

    @Test("行缺席（fresh spawn 未入轮询）→ 回退 controller.spawnCwd；bootstrap 绝不复注入")
    func newInCwdFallsBackToSpawnCwdWithoutBootstrap() async throws {
        // Arrange：项目内 fresh spawn（spawnCwd + claude bootstrap），列表无行。
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.spawnCwd")
        let coordinator = AppCoordinator(environment: fixture.environment)
        coordinator.openProject(ProjectOpenRequest(
            id: UUID(), host: fixture.host, cwd: "/repos/api",
            bootstrapInput: ProjectLaunch.claudeBootstrapInput
        ))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: UUID())

        // Act
        coordinator.openNewSessionInCurrentCwd()

        // Assert：同 cwd，但只有裸 attach —— claude\r 不属于"开新 shell"。
        let second = try #require(coordinator.terminalController)
        await awaitAttachSettled(second)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection.count == 2)
        #expect(byConnection[1] == [
            MessageCodec.encode(.attach(sessionId: nil, cwd: "/repos/api"))
        ])
        second.teardown()
    }

    @Test("cwd 全未知（无行、无 spawnCwd）→ 普通新会话 attach(null, nil)")
    func newInCwdWithUnknownCwdOpensPlainSession() async throws {
        // Arrange：直接"+ 新会话"打开（无列表行），adopted id 不在任何行里。
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.unknown")
        let coordinator = AppCoordinator(environment: fixture.environment)
        coordinator.open(SessionListViewModel.OpenRequest(
            id: UUID(), host: fixture.host, sessionId: nil
        ))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: UUID())

        // Act
        coordinator.openNewSessionInCurrentCwd()

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

    @Test("服务器行 cwd 非绝对路径（不可信输入）→ 按未知处理，attach(null, nil)")
    func hostileRelativeCwdIsTreatedAsUnknown() async throws {
        // Arrange：敌意/损坏的服务器数据 —— cwd 不是绝对路径。
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.hostile")
        let coordinator = AppCoordinator(environment: fixture.environment)
        let sessionId = UUID()
        try await primeRows(
            coordinator, fixture: fixture,
            rows: [sessionJSON(id: sessionId, cwd: "repos/../etc")]
        )
        coordinator.open(SessionListViewModel.OpenRequest(
            id: UUID(), host: fixture.host, sessionId: sessionId
        ))
        let first = try #require(coordinator.terminalController)
        await adopt(first, transport: fixture.transport, sessionId: sessionId)

        // Act
        coordinator.openNewSessionInCurrentCwd()

        // Assert：非法 cwd 绝不透传。
        let second = try #require(coordinator.terminalController)
        await awaitAttachSettled(second)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection[1] == [MessageCodec.encode(.attach(sessionId: nil, cwd: nil))])
        second.teardown()
    }

    @Test("无打开的终端 → 动作 no-op（不 spawn、不连接）")
    func actionWithoutOpenTerminalIsNoOp() async throws {
        // Arrange
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.noop")
        let coordinator = AppCoordinator(environment: fixture.environment)

        // Act
        coordinator.openNewSessionInCurrentCwd()

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

    // MARK: - 2. exited 会话：回放 + exit 横幅 + 横幅"开新会话"

    @Test("exited 会话点开：回放渲染 + exit 横幅只读 → 动作以该行 cwd 开新会话")
    func exitedSessionReplaysThenBannerActionReusesCwd() async throws {
        // Arrange：列表里的 exited 行（manager.ts:145-153——reap 前仍可列出）。
        let fixture = try makeFixture(suiteName: "NewSessionInCwdTests.exited")
        let coordinator = AppCoordinator(environment: fixture.environment)
        let sessionId = UUID()
        try await primeRows(
            coordinator, fixture: fixture,
            rows: [sessionJSON(id: sessionId, cwd: "/Users/dev/proj", exited: true)]
        )
        coordinator.open(SessionListViewModel.OpenRequest(
            id: UUID(), host: fixture.host, sessionId: sessionId
        ))
        let first = try #require(coordinator.terminalController)
        var fed: [String] = []
        first.terminalViewModel.attachTerminalSink { fed.append($0) }
        await first.openTask?.value

        // Act：服务器语义 —— 回放 ring buffer，随后补发 exit。
        await fixture.transport.emit(
            frame: #"{"type":"attached","sessionId":"\#(sessionId.uuidString.lowercased())"}"#
        )
        await fixture.transport.emit(frame: #"{"type":"output","data":"[replay] $ make done"}"#)
        await fixture.transport.emit(frame: #"{"type":"exit","code":0}"#)
        await first.terminalViewModel.waitUntilProcessed(eventCount: 5)

        // Assert：回放已渲染、exit 横幅只读。
        #expect(fed == ["[replay] $ make done"])
        #expect(first.terminalViewModel.bannerModel == .exited(code: 0, reason: nil))
        #expect(first.terminalViewModel.isReadOnly)

        // Act：横幅"开新会话"（同一 coordinator 动作）。
        coordinator.openNewSessionInCurrentCwd()

        // Assert：同 cwd 的 fresh spawn。
        let second = try #require(coordinator.terminalController)
        await awaitAttachSettled(second)
        let byConnection = await fixture.transport.sentFramesByConnection
        #expect(byConnection[1] == [
            MessageCodec.encode(.attach(sessionId: nil, cwd: "/Users/dev/proj"))
        ])
        second.teardown()
    }

    @Test("横幅'开新会话'只在 exited 态可用（failed/连接态不提供）")
    func bannerNewSessionAffordanceOnlyWhenExited() {
        #expect(ReconnectBanner.isNewSessionActionAvailable(for: .exited(code: 0, reason: nil)))
        #expect(ReconnectBanner.isNewSessionActionAvailable(
            for: .exited(code: -1, reason: "spawn failed")
        ))
        #expect(!ReconnectBanner.isNewSessionActionAvailable(for: .connecting))
        #expect(!ReconnectBanner.isNewSessionActionAvailable(
            for: .reconnecting(attempt: 1, retryIn: .seconds(1))
        ))
        #expect(!ReconnectBanner.isNewSessionActionAvailable(for: .failed(message: "x")))
    }
}
