import Foundation
import Testing
import TestSupport
import WireProtocol
import APIClient

/// T-iOS-38 · APNs device-token 注册端点（orchestrator 冻结 wire shape）：
/// - `POST /push/apns-token`   body `{"token":"<64-160 位小写 hex>"}` → 204（幂等 upsert）
/// - `DELETE /push/apns-token` body 同形 → 204（幂等；未知 token 仍 204）
/// 两者均为 G 端点（Origin 逐字符等于 `endpoint.originHeader`）；服务器约束：
/// 400 非法 token/形状 · 403 Origin 守卫 · 429 限频 5 次/分/IP · body ≤ 8 KB
///（对齐 POST /push/subscribe 约定，src/server.ts:461-480,73）。
/// hex 校验镜像服务器规则并在**联网前**拒绝（客户端边界验证，plan §4）。
struct ApnsTokenTests {
    private static let base = "http://192.168.1.5:3000"
    /// 64 位小写 hex —— 典型 APNs device token（32 字节）。
    private static let validToken = String(repeating: "0123456789abcdef", count: 4)

    private struct Fixture {
        let endpoint: HostEndpoint
        let http: FakeHTTPTransport
        let client: APIClient
        let url: URL
    }

    private func makeFixture() throws -> Fixture {
        let baseURL = try #require(URL(string: Self.base))
        let endpoint = try #require(HostEndpoint(baseURL: baseURL))
        let http = FakeHTTPTransport()
        return Fixture(
            endpoint: endpoint,
            http: http,
            client: APIClient(endpoint: endpoint, http: http),
            url: try #require(URL(string: Self.base + "/push/apns-token"))
        )
    }

    // MARK: - 冻结 wire shape（方法/路径/Origin/body）

    @Test("register 为 POST /push/apns-token,G 端点:Origin 逐字符等于 endpoint.originHeader,body 恰为 {token}")
    func registerBuildsFrozenWireShape() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 204)

        // Act
        try await fixture.client.registerApnsToken(Self.validToken)

        // Assert
        let request = try #require(await fixture.http.recordedRequests.first)
        #expect(request.httpMethod == "POST")
        #expect(request.url == fixture.url)
        #expect(request.value(forHTTPHeaderField: "Origin") == fixture.endpoint.originHeader)
        #expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")
        let body = try #require(request.httpBody)
        let object = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
        #expect(Set(object.keys) == Set(["token"]))
        #expect(object["token"] as? String == Self.validToken)
    }

    @Test("unregister 为 DELETE /push/apns-token,同 G 守卫同 body 形状,204 → 成功(幂等)")
    func unregisterBuildsFrozenWireShape() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "DELETE", url: fixture.url, status: 204)

        // Act
        try await fixture.client.unregisterApnsToken(Self.validToken)

        // Assert
        let request = try #require(await fixture.http.recordedRequests.first)
        #expect(request.httpMethod == "DELETE")
        #expect(request.url == fixture.url)
        #expect(request.value(forHTTPHeaderField: "Origin") == fixture.endpoint.originHeader)
        let body = try #require(request.httpBody)
        let object = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
        #expect(object["token"] as? String == Self.validToken)
    }

    // MARK: - 客户端 hex 校验（镜像服务器规则，联网前拒绝）

    @Test("大写 hex 输入 → 联网前归一化为小写(服务器按小写归一存储)")
    func uppercaseTokenIsLowercaseNormalizedBeforeNetwork() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 204)

        // Act
        try await fixture.client.registerApnsToken(Self.validToken.uppercased())

        // Assert
        let request = try #require(await fixture.http.recordedRequests.first)
        let body = try #require(request.httpBody)
        let object = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
        #expect(object["token"] as? String == Self.validToken)
    }

    @Test("非法 token(过短/过长/非 hex/空/含空格) → invalidApnsToken 且零网络请求")
    func invalidTokensAreRejectedBeforeAnyNetworkRequest() async throws {
        // Arrange — 63 位(短 1)、161 位(长 1)、非 hex 字符、空串、内嵌空格
        let fixture = try makeFixture()
        let invalidTokens = [
            String(repeating: "a", count: 63),
            String(repeating: "a", count: 161),
            String(repeating: "g", count: 64),
            "",
            String(repeating: "a", count: 32) + " " + String(repeating: "a", count: 31),
        ]

        // Act + Assert — register 与 unregister 两条路径都在联网前拒绝
        for token in invalidTokens {
            await #expect(throws: APIClientError.invalidApnsToken) {
                try await fixture.client.registerApnsToken(token)
            }
            await #expect(throws: APIClientError.invalidApnsToken) {
                try await fixture.client.unregisterApnsToken(token)
            }
        }
        #expect(await fixture.http.recordedRequests.isEmpty)
    }

    @Test("边界长度 64 与 160 位 hex 均接受(服务器规则 64-160)")
    func boundaryLengthTokensAreAccepted() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 204)
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 204)

        // Act
        try await fixture.client.registerApnsToken(String(repeating: "f", count: 64))
        try await fixture.client.registerApnsToken(String(repeating: "f", count: 160))

        // Assert
        #expect(await fixture.http.recordedRequests.count == 2)
    }

    // MARK: - 状态码映射

    @Test("400(服务器判 token 非法) → invalidApnsToken;403(Origin 守卫) → forbidden")
    func status400And403MapToTypedErrors() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 400)
        await fixture.http.queueSuccess(method: "DELETE", url: fixture.url, status: 403)

        // Act + Assert
        await #expect(throws: APIClientError.invalidApnsToken) {
            try await fixture.client.registerApnsToken(Self.validToken)
        }
        await #expect(throws: APIClientError.forbidden) {
            try await fixture.client.unregisterApnsToken(Self.validToken)
        }
    }

    @Test("429 → rateLimited(≤5 次/分/IP,对齐 push/subscribe 限频);500 → unexpectedStatus")
    func status429And500MapToTypedErrors() async throws {
        // Arrange
        let fixture = try makeFixture()
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 429)
        await fixture.http.queueSuccess(method: "POST", url: fixture.url, status: 500)

        // Act + Assert
        await #expect(throws: APIClientError.rateLimited) {
            try await fixture.client.registerApnsToken(Self.validToken)
        }
        await #expect(throws: APIClientError.unexpectedStatus(500)) {
            try await fixture.client.registerApnsToken(Self.validToken)
        }
    }

    @Test("invalidApnsToken 有非空 UI 话术(plan §4 错误处理全面显式)")
    func invalidApnsTokenHasNonEmptyMessage() {
        #expect(!APIClientError.invalidApnsToken.message.isEmpty)
    }
}
