import APIClient
import Foundation
import HostRegistry
import Testing
import TestSupport
import UserNotifications
import WireProtocol
@testable import WebTerm

/// T-iOS-21 · WEBTERM_GATE category 注册形状（安全注：Allow 必带
/// `.authenticationRequired`、两动作均不带 `.foreground`）。
@MainActor
@Suite("GateNotificationCategory")
struct GateNotificationCategoryTests {
    @Test("category id 与服务器 GATE_CATEGORY 一致（src/push/apns.ts:52）")
    func categoryIdentifierMatchesServer() {
        #expect(GateNotificationCategory.category().identifier == "WEBTERM_GATE")
    }

    @Test("恰好两动作，顺序 Allow→Deny，标题为中文具名常量")
    func actionsShapeAndTitles() {
        let actions = GateNotificationCategory.category().actions
        #expect(actions.count == 2)
        #expect(actions.first?.identifier == GateNotificationCategory.allowActionId)
        #expect(actions.last?.identifier == GateNotificationCategory.denyActionId)
        #expect(actions.first?.title == GateNotificationCategory.allowTitle)
        #expect(actions.last?.title == GateNotificationCategory.denyTitle)
    }

    @Test("安全注：Allow 必须带 .authenticationRequired（锁屏批准 = 授权主机执行命令）")
    func allowRequiresAuthentication() {
        let allow = GateNotificationCategory.category().actions.first
        #expect(allow?.options.contains(.authenticationRequired) == true)
    }

    @Test("安全注：Deny 保持免认证（fail-safe——旁观者只能拒绝）")
    func denyStaysUnauthenticated() {
        let deny = GateNotificationCategory.category().actions.last
        #expect(deny?.options.contains(.authenticationRequired) == false)
    }

    @Test("安全注：两动作都不带 .foreground（锁屏两次手势闭环，不拉起 UI）")
    func neitherActionForegrounds() {
        let actions = GateNotificationCategory.category().actions
        for action in actions {
            #expect(!action.options.contains(.foreground))
        }
    }
}

/// T-iOS-21 · PushRegistrar：授权流程（真授权而非 provisional，因锁屏动作需要
/// alert 级展示）、device token → 每个已配对主机逐一注册、失败续命、移除钩子。
@MainActor
@Suite("PushRegistrar")
struct PushRegistrarTests {
    // MARK: - Fakes（seam 替身；真 UNUserNotificationCenter 无法在单测实例化流程）

    @MainActor
    private final class FakeNotificationCenter: NotificationCenterClient {
        var status: UNAuthorizationStatus = .notDetermined
        var grantResult: Result<Bool, any Error> = .success(true)
        private(set) var requestedOptions: [UNAuthorizationOptions] = []
        private(set) var registeredCategorySets: [Set<UNNotificationCategory>] = []
        private(set) var addedRequests: [UNNotificationRequest] = []

        func authorizationStatus() async -> UNAuthorizationStatus { status }

        func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
            requestedOptions.append(options)
            return try grantResult.get()
        }

        func setNotificationCategories(_ categories: Set<UNNotificationCategory>) {
            registeredCategorySets.append(categories)
        }

        func add(_ request: UNNotificationRequest) async throws {
            addedRequests.append(request)
        }
    }

    @MainActor
    private final class FakeRemoteRegistrar: RemoteNotificationRegistering {
        private(set) var registerCallCount = 0
        func registerForRemoteNotifications() { registerCallCount += 1 }
    }

    private enum StubError: Error { case authorizationFailed }

    // MARK: - Fixtures

    private static let hostABase = "http://192.168.1.5:3000"
    private static let hostBBase = "http://192.168.1.6:3000"
    /// 32 字节 device token → 64 位小写 hex（APNs 现行长度）。
    private static let tokenData = Data((0..<32).map { UInt8($0) })
    private static let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()

    private static func makeHost(base: String, name: String = "mac") throws -> HostRegistry.Host {
        let url = try #require(URL(string: base))
        let endpoint = try #require(HostEndpoint(baseURL: url))
        return HostRegistry.Host(id: UUID(), name: name, endpoint: endpoint)
    }

    private static func apnsTokenURL(base: String) throws -> URL {
        try #require(URL(string: "\(base)/push/apns-token"))
    }

    private static func makeRegistrar(
        hosts: [HostRegistry.Host],
        http: FakeHTTPTransport,
        center: FakeNotificationCenter,
        remote: FakeRemoteRegistrar
    ) -> PushRegistrar {
        PushRegistrar(
            hostStore: InMemoryHostStore(hosts: hosts),
            http: http,
            center: center,
            remote: remote
        )
    }

    // MARK: - activate()：授权流程

    @Test("activate → 注册含 WEBTERM_GATE 的 category 集合")
    func activateRegistersGateCategory() async throws {
        let center = FakeNotificationCenter()
        let registrar = Self.makeRegistrar(
            hosts: [], http: FakeHTTPTransport(), center: center, remote: FakeRemoteRegistrar()
        )

        await registrar.activate()

        let registered = try #require(center.registeredCategorySets.first)
        #expect(registered.contains(where: { $0.identifier == GateNotificationCategory.identifier }))
    }

    @Test("无已配对主机 → 不请求授权、不注册远程通知（无推送来源不打扰）")
    func activateWithoutHostsSkipsAuthorization() async {
        let center = FakeNotificationCenter()
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [], http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(center.requestedOptions.isEmpty)
        #expect(remote.registerCallCount == 0)
    }

    @Test("有主机 + notDetermined + 授予 → 以 [.alert,.sound] 请求（真授权，非 provisional）并注册远程通知")
    func activateRequestsRealAuthorizationAndRegisters() async throws {
        let center = FakeNotificationCenter()
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [try Self.makeHost(base: Self.hostABase)],
            http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(center.requestedOptions == [[.alert, .sound]])
        // provisional 只静默进通知中心，锁屏 Allow/Deny 需要 alert 级授权。
        #expect(center.requestedOptions.first?.contains(.provisional) == false)
        #expect(remote.registerCallCount == 1)
    }

    @Test("用户拒绝授权 → 不注册远程通知、不 crash")
    func activateDeniedGrantSkipsRegistration() async throws {
        let center = FakeNotificationCenter()
        center.grantResult = .success(false)
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [try Self.makeHost(base: Self.hostABase)],
            http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(remote.registerCallCount == 0)
    }

    @Test("授权请求抛错 → 记日志、不注册、不 crash")
    func activateAuthorizationErrorHandled() async throws {
        let center = FakeNotificationCenter()
        center.grantResult = .failure(StubError.authorizationFailed)
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [try Self.makeHost(base: Self.hostABase)],
            http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(remote.registerCallCount == 0)
    }

    @Test("状态已 denied → 不再弹请求，也不注册")
    func activateDeniedStatusShortCircuits() async throws {
        let center = FakeNotificationCenter()
        center.status = .denied
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [try Self.makeHost(base: Self.hostABase)],
            http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(center.requestedOptions.isEmpty)
        #expect(remote.registerCallCount == 0)
    }

    @Test("状态已 authorized → 跳过请求直接注册远程通知")
    func activateAuthorizedStatusRegistersDirectly() async throws {
        let center = FakeNotificationCenter()
        center.status = .authorized
        let remote = FakeRemoteRegistrar()
        let registrar = Self.makeRegistrar(
            hosts: [try Self.makeHost(base: Self.hostABase)],
            http: FakeHTTPTransport(), center: center, remote: remote
        )

        await registrar.activate()

        #expect(center.requestedOptions.isEmpty)
        #expect(remote.registerCallCount == 1)
    }

    // MARK: - handleDeviceToken：逐主机注册

    @Test("device token → 小写 hex，并对每个已配对主机 POST /push/apns-token（带 Origin）")
    func deviceTokenRegistersWithEveryHost() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let hostB = try Self.makeHost(base: Self.hostBBase, name: "mini")
        let http = FakeHTTPTransport()
        await http.queueSuccess(
            method: "POST", url: try Self.apnsTokenURL(base: Self.hostABase), status: 204
        )
        await http.queueSuccess(
            method: "POST", url: try Self.apnsTokenURL(base: Self.hostBBase), status: 204
        )
        let registrar = Self.makeRegistrar(
            hosts: [hostA, hostB], http: http,
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )

        await registrar.handleDeviceToken(Self.tokenData)

        #expect(registrar.currentTokenHex == Self.tokenHex)
        let requests = await http.recordedRequests
        #expect(requests.count == 2)
        let urls = Set(requests.compactMap { $0.url?.absoluteString })
        #expect(urls == [
            "\(Self.hostABase)/push/apns-token", "\(Self.hostBBase)/push/apns-token",
        ])
        for request in requests {
            #expect(request.httpMethod == "POST")
            // G 端点铁律：Origin 必与 endpoint.originHeader 逐字节一致。
            let origin = request.value(forHTTPHeaderField: "Origin")
            #expect(origin == request.url?.absoluteString.replacingOccurrences(
                of: "/push/apns-token", with: ""
            ))
            let body = try #require(request.httpBody)
            let decoded = try JSONDecoder().decode([String: String].self, from: body)
            #expect(decoded == ["token": Self.tokenHex])
        }
    }

    @Test("一主机失败 → 其余主机照常注册；同 token 重试只补失败的主机")
    func failedHostRetriedNextTime() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let hostB = try Self.makeHost(base: Self.hostBBase, name: "mini")
        let http = FakeHTTPTransport()
        let urlA = try Self.apnsTokenURL(base: Self.hostABase)
        let urlB = try Self.apnsTokenURL(base: Self.hostBBase)
        await http.queueFailure(method: "POST", url: urlA, error: URLError(.cannotConnectToHost))
        await http.queueSuccess(method: "POST", url: urlB, status: 204)
        let registrar = Self.makeRegistrar(
            hosts: [hostA, hostB], http: http,
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )

        await registrar.handleDeviceToken(Self.tokenData)
        #expect(await http.recordedRequests.count == 2)

        // 第二次同 token 送达（下次启动/激活的重试路径）：只补 hostA。
        await http.queueSuccess(method: "POST", url: urlA, status: 204)
        await registrar.handleDeviceToken(Self.tokenData)

        let requests = await http.recordedRequests
        #expect(requests.count == 3)
        #expect(requests.last?.url == urlA)
    }

    @Test("token 变化 → 对全部主机重新注册")
    func tokenChangeReRegistersAllHosts() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let http = FakeHTTPTransport()
        let urlA = try Self.apnsTokenURL(base: Self.hostABase)
        await http.queueSuccess(method: "POST", url: urlA, status: 204)
        await http.queueSuccess(method: "POST", url: urlA, status: 204)
        let registrar = Self.makeRegistrar(
            hosts: [hostA], http: http,
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )

        await registrar.handleDeviceToken(Self.tokenData)
        let changed = Data((0..<32).map { UInt8($0 &+ 1) })
        await registrar.handleDeviceToken(changed)

        #expect(await http.recordedRequests.count == 2)
        #expect(registrar.currentTokenHex == changed.map { String(format: "%02x", $0) }.joined())
    }

    // MARK: - 主机移除钩子（additive hook：目前无 UI 移除路径，见任务决策记录）

    @Test("handleHostRemoved（有 token）→ 对该主机 DELETE /push/apns-token")
    func hostRemovedUnregistersToken() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let http = FakeHTTPTransport()
        let urlA = try Self.apnsTokenURL(base: Self.hostABase)
        await http.queueSuccess(method: "POST", url: urlA, status: 204)
        await http.queueSuccess(method: "DELETE", url: urlA, status: 204)
        let registrar = Self.makeRegistrar(
            hosts: [hostA], http: http,
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )
        await registrar.handleDeviceToken(Self.tokenData)

        await registrar.handleHostRemoved(hostA)

        let last = try #require(await http.recordedRequests.last)
        #expect(last.httpMethod == "DELETE")
        #expect(last.url == urlA)
        let body = try #require(last.httpBody)
        #expect(try JSONDecoder().decode([String: String].self, from: body)
            == ["token": Self.tokenHex])
    }

    @Test("handleHostRemoved（无 token）→ 不发任何请求")
    func hostRemovedWithoutTokenIsNoOp() async throws {
        let hostA = try Self.makeHost(base: Self.hostABase)
        let http = FakeHTTPTransport()
        let registrar = Self.makeRegistrar(
            hosts: [hostA], http: http,
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )

        await registrar.handleHostRemoved(hostA)

        #expect(await http.recordedRequests.isEmpty)
    }

    @Test("远程注册失败回调 → 只记日志，不 crash")
    func registrationFailureLoggedOnly() throws {
        let registrar = Self.makeRegistrar(
            hosts: [], http: FakeHTTPTransport(),
            center: FakeNotificationCenter(), remote: FakeRemoteRegistrar()
        )

        registrar.handleRegistrationFailure(URLError(.notConnectedToInternet))

        #expect(registrar.currentTokenHex == nil)
    }
}
