import Foundation
import Testing
import TestSupport
import WireProtocol
import APIClient

/// T-iOS-8 · 响应模型解码。服务器是**不可信输入源**（plan §4）：
/// - `LiveSessionInfo` 镜像 src/types.ts:246-256，`telemetry` 可缺失/为 null；
/// - 未知 `status` → `.unknown`（新状态值不得让运行中的会话从列表里消失）；
/// - 畸形条目丢弃、整体不 crash；非数组 body → 显式错误（探针的"端口对吗?"信号）；
/// - `TimelineEvent` 经 WireProtocol 的 `decodeList` 帮助器，未知 `class` 该条丢弃。
struct ModelDecodingTests {
    private static let base = "http://192.168.1.5:3000"
    private static let sessionIdString = "0f5a1b2c-3d4e-4f60-8a9b-0c1d2e3f4a5b"

    private static let fullSessionJSON = """
    {"id":"\(sessionIdString)","createdAt":1720000000000,"clientCount":2,\
    "status":"working","exited":false,"cwd":"/Users/dev/proj","cols":120,"rows":40,\
    "telemetry":{"contextUsedPct":42.5,"costUsd":1.25,"linesAdded":10,"linesRemoved":2,\
    "model":"Opus","effort":"high",\
    "pr":{"number":7,"url":"https://github.com/x/y/pull/7","reviewState":"APPROVED"},\
    "rate":{"fiveHourPct":12.5,"sevenDayPct":40.0},"at":1720000005000}}
    """

    private static let noTelemetrySessionJSON = """
    {"id":"\(sessionIdString)","createdAt":1720000000000,"clientCount":0,\
    "status":"idle","exited":false,"cwd":null,"cols":80,"rows":24}
    """

    private func makeClient(_ http: FakeHTTPTransport) throws -> APIClient {
        let url = try #require(URL(string: Self.base))
        let endpoint = try #require(HostEndpoint(baseURL: url))
        return APIClient(endpoint: endpoint, http: http)
    }

    private func routeURL(_ path: String) throws -> URL {
        try #require(URL(string: Self.base + path))
    }

    private func fetchSessions(body: String) async throws -> [LiveSessionInfo] {
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(url: try routeURL("/live-sessions"), body: Data(body.utf8))
        return try await client.liveSessions()
    }

    // MARK: - LiveSessionInfo

    @Test("LiveSessionInfo 全字段样本解码(src/types.ts:246-256 形状,含完整 telemetry)")
    func liveSessionInfoDecodesFullSample() async throws {
        // Act
        let sessions = try await fetchSessions(body: "[\(Self.fullSessionJSON)]")

        // Assert
        #expect(sessions.count == 1)
        let session = try #require(sessions.first)
        #expect(session.id == UUID(uuidString: Self.sessionIdString))
        #expect(session.createdAt == 1_720_000_000_000)
        #expect(session.clientCount == 2)
        #expect(session.status == .working)
        #expect(session.exited == false)
        #expect(session.cwd == "/Users/dev/proj")
        #expect(session.cols == 120)
        #expect(session.rows == 40)
        let telemetry = try #require(session.telemetry)
        #expect(telemetry.contextUsedPct == 42.5)
        #expect(telemetry.costUsd == 1.25)
        #expect(telemetry.linesAdded == 10)
        #expect(telemetry.linesRemoved == 2)
        #expect(telemetry.model == "Opus")
        #expect(telemetry.effort == "high")
        #expect(telemetry.pr?.number == 7)
        #expect(telemetry.rate?.fiveHourPct == 12.5)
        #expect(telemetry.at == 1_720_000_005_000)
        // 与 memberwise 构造的期望值整体相等(Equatable 全字段往返)
        let expected = LiveSessionInfo(
            id: try #require(UUID(uuidString: Self.sessionIdString)),
            createdAt: 1_720_000_000_000,
            clientCount: 2,
            status: .working,
            exited: false,
            cwd: "/Users/dev/proj",
            cols: 120,
            rows: 40,
            telemetry: StatusTelemetry(
                contextUsedPct: 42.5, costUsd: 1.25, linesAdded: 10, linesRemoved: 2,
                model: "Opus", effort: "high",
                pr: PrInfo(number: 7, url: "https://github.com/x/y/pull/7", reviewState: "APPROVED"),
                rate: RateInfo(fiveHourPct: 12.5, sevenDayPct: 40.0),
                at: 1_720_000_005_000
            )
        )
        #expect(session == expected)
    }

    @Test("LiveSessionInfo telemetry 缺失/cwd 为 null → 解码为 nil,其余字段完整")
    func liveSessionInfoToleratesMissingTelemetryAndNullCwd() async throws {
        // Act
        let sessions = try await fetchSessions(body: "[\(Self.noTelemetrySessionJSON)]")

        // Assert
        let session = try #require(sessions.first)
        #expect(session.telemetry == nil)
        #expect(session.cwd == nil)
        #expect(session.status == .idle)
        #expect(session.cols == 80)
        #expect(session.rows == 24)
    }

    @Test("LiveSessionInfo.lastOutputAt 可选字段(T-iOS-37/23):在场→解出;缺席(旧服务器)→nil;坏类型→nil 不丢条目")
    func lastOutputAtDecodesAsOptional() async throws {
        // Arrange: 在场 / 缺席 / 坏类型（不可信服务器）三个条目。
        let present = Self.noTelemetrySessionJSON
            .replacingOccurrences(of: #""createdAt":1720000000000"#,
                                  with: #""createdAt":1720000000000,"lastOutputAt":1720000007000"#)
        let wrongType = Self.fullSessionJSON
            .replacingOccurrences(of: #""createdAt":1720000000000"#,
                                  with: #""createdAt":1720000000000,"lastOutputAt":"soon""#)

        // Act
        let sessions = try await fetchSessions(
            body: "[\(present),\(Self.noTelemetrySessionJSON),\(wrongType)]"
        )

        // Assert
        #expect(sessions.count == 3)
        #expect(sessions[0].lastOutputAt == 1_720_000_007_000)
        #expect(sessions[1].lastOutputAt == nil)
        #expect(sessions[2].lastOutputAt == nil)
    }

    @Test("未知 status 字符串 → .unknown(不丢弃整个会话条目)")
    func unknownStatusStringMapsToUnknownWithoutDroppingEntry() async throws {
        // Arrange
        let body = Self.noTelemetrySessionJSON
            .replacingOccurrences(of: #""status":"idle""#, with: #""status":"hyperdrive""#)

        // Act
        let sessions = try await fetchSessions(body: "[\(body)]")

        // Assert
        #expect(sessions.count == 1)
        #expect(sessions.first?.status == .unknown)
    }

    @Test("畸形条目(非对象/坏 UUID/缺必填)逐条丢弃,合法条目保留,不 crash")
    func malformedEntriesAreDroppedWhileValidOnesSurvive() async throws {
        // Arrange
        let badUUID = Self.noTelemetrySessionJSON
            .replacingOccurrences(of: Self.sessionIdString, with: "not-a-uuid")
        let body = "[\(Self.fullSessionJSON),42,\(badUUID),{\"id\":\"x\"}]"

        // Act
        let sessions = try await fetchSessions(body: body)

        // Assert
        #expect(sessions.count == 1)
        #expect(sessions.first?.id == UUID(uuidString: Self.sessionIdString))
    }

    @Test("非数组 body(HTML/对象)→ invalidResponseBody 显式错误(探针的 端口对吗 信号)")
    func nonArrayBodyThrowsInvalidResponseBody() async throws {
        // Act + Assert — HTML(路由器管理页之类)
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await self.fetchSessions(body: "<html><body>router admin</body></html>")
        }
        // Act + Assert — JSON 但不是数组
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await self.fetchSessions(body: #"{"error":"nope"}"#)
        }
    }

    // MARK: - TimelineEvent（经 WireProtocol helper）

    @Test("TimelineEvent 未知 class 值 → 该条丢弃不 crash(服务器视为不可信)")
    func unknownTimelineClassEntriesAreDroppedNotCrashed() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        let body = """
        [{"at":1,"class":"tool","toolName":"Bash","label":"ran Bash"},\
        {"at":2,"class":"quantum","label":"??"},\
        {"at":3,"class":"waiting","label":"waiting for approval"}]
        """
        await http.queueSuccess(
            url: try routeURL("/live-sessions/\(Self.sessionIdString)/events"),
            body: Data(body.utf8)
        )

        // Act
        let events = try await client.events(id: try #require(UUID(uuidString: Self.sessionIdString)))

        // Assert
        #expect(events.count == 2)
        #expect(events.map(\.class) == ["tool", "waiting"])
        #expect(events.first?.toolName == "Bash")
    }

    @Test("events 404 → sessionNotFound")
    func events404ThrowsSessionNotFound() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(
            url: try routeURL("/live-sessions/\(Self.sessionIdString)/events"),
            status: 404
        )

        // Act + Assert
        await #expect(throws: APIClientError.sessionNotFound) {
            _ = try await client.events(id: try #require(UUID(uuidString: Self.sessionIdString)))
        }
    }

    // MARK: - SessionPreview / UiConfig

    @Test("SessionPreview 全字段解码(GET /live-sessions/:id/preview 形状)")
    func sessionPreviewDecodesAllFields() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        let body = #"{"id":"\#(Self.sessionIdString)","cols":100,"rows":30,"data":"hello"}"#
        await http.queueSuccess(
            url: try routeURL("/live-sessions/\(Self.sessionIdString)/preview"),
            body: Data(body.utf8)
        )

        // Act
        let preview = try await client.preview(id: try #require(UUID(uuidString: Self.sessionIdString)))

        // Assert
        let expected = SessionPreview(
            id: try #require(UUID(uuidString: Self.sessionIdString)),
            cols: 100, rows: 30, data: "hello"
        )
        #expect(preview == expected)
    }

    @Test("preview 200 但 body 非预期形状 → invalidResponseBody")
    func previewGarbageBodyThrowsInvalidResponseBody() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(
            url: try routeURL("/live-sessions/\(Self.sessionIdString)/preview"),
            body: Data("not json".utf8)
        )

        // Act + Assert
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await client.preview(id: try #require(UUID(uuidString: Self.sessionIdString)))
        }
    }

    @Test("preview 404 → sessionNotFound")
    func preview404ThrowsSessionNotFound() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(
            url: try routeURL("/live-sessions/\(Self.sessionIdString)/preview"),
            status: 404
        )

        // Act + Assert
        await #expect(throws: APIClientError.sessionNotFound) {
            _ = try await client.preview(id: try #require(UUID(uuidString: Self.sessionIdString)))
        }
    }

    @Test("UiConfig 解码 allowAutoMode(GET /config/ui,src/types.ts:503)")
    func uiConfigDecodesAllowAutoMode() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(
            url: try routeURL("/config/ui"),
            body: Data(#"{"allowAutoMode":true}"#.utf8)
        )

        // Act
        let config = try await client.uiConfig()

        // Assert
        #expect(config == UiConfig(allowAutoMode: true))
    }

    @Test("uiConfig 200 但 body 非预期形状 → invalidResponseBody")
    func uiConfigGarbageBodyThrowsInvalidResponseBody() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(url: try routeURL("/config/ui"), body: Data("[]".utf8))

        // Act + Assert
        await #expect(throws: APIClientError.invalidResponseBody) {
            _ = try await client.uiConfig()
        }
    }

    @Test("liveSessions 非 200(500) → unexpectedStatus(500)")
    func liveSessions500ThrowsUnexpectedStatus() async throws {
        // Arrange
        let http = FakeHTTPTransport()
        let client = try makeClient(http)
        await http.queueSuccess(url: try routeURL("/live-sessions"), status: 500)

        // Act + Assert
        await #expect(throws: APIClientError.unexpectedStatus(500)) {
            _ = try await client.liveSessions()
        }
    }

    // MARK: - 话术

    @Test("decisionRejected 话术明示 token 已过期/已被处理(RED 清单:403 → token 过期话术)")
    func decisionRejectedMessageMentionsTokenExpiry() {
        #expect(APIClientError.decisionRejected.message.contains("过期"))
    }

    @Test("APIClientError 每个 case 都有非空 UI 话术(plan §4:错误处理全面显式)")
    func everyAPIClientErrorCaseHasNonEmptyMessage() {
        // Arrange
        let allCases: [APIClientError] = [
            .invalidRequest, .invalidResponseBody, .sessionNotFound,
            .forbidden, .decisionRejected, .rateLimited, .unexpectedStatus(500),
        ]

        // Act + Assert
        for errorCase in allCases {
            #expect(!errorCase.message.isEmpty)
        }
        #expect(APIClientError.unexpectedStatus(500).message.contains("500"))
    }
}
