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

/// T-iOS-21 · `NotificationActionHandler.parse`：payload 级白名单解析
///（push payload 是不可信外部输入——sessionId 复用 DeepLinkRouter 的冻结
/// v4 校验，token 校验 v4 形状后原样透传，任一非法 → .invalidPayload）。
@MainActor
@Suite("NotificationActionHandler.parse")
struct NotificationActionParseTests {
    private static let sessionId = "0f5a1f2e-3b4c-4d5e-8f6a-7b8c9d0e1f2a"
    /// 服务器 capability token = crypto.randomUUID()（src/server.ts:445）→ v4 形状。
    private static let token = "7c1de1e0-9a2b-4c3d-8e4f-5a6b7c8d9e0f"

    private static func gateUserInfo(
        sessionId: String = sessionId, token: Any? = token
    ) -> [AnyHashable: Any] {
        var info: [AnyHashable: Any] = [
            "aps": ["category": "WEBTERM_GATE"],
            "sessionId": sessionId,
            "cls": "needs-input",
        ]
        if let token { info["token"] = token }
        return info
    }

    @Test("Allow 动作 + 合法 payload → .decision(.allow)")
    func parseAllowAction() throws {
        let parsed = NotificationActionHandler.parse(
            actionIdentifier: GateNotificationCategory.allowActionId,
            userInfo: Self.gateUserInfo()
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))
        #expect(parsed == .decision(.allow, sessionId: sessionId, token: Self.token))
    }

    @Test("Deny 动作 + 合法 payload → .decision(.deny)")
    func parseDenyAction() throws {
        let parsed = NotificationActionHandler.parse(
            actionIdentifier: GateNotificationCategory.denyActionId,
            userInfo: Self.gateUserInfo()
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))
        #expect(parsed == .decision(.deny, sessionId: sessionId, token: Self.token))
    }

    @Test("决策动作缺 token / token 非字符串 / token 非 v4 形状 → .invalidPayload")
    func parseDecisionRejectsBadToken() {
        for bad in [nil, 42 as Any, "not-a-token", "11111111-2222-1333-8444-555555555555"] {
            let parsed = NotificationActionHandler.parse(
                actionIdentifier: GateNotificationCategory.allowActionId,
                userInfo: Self.gateUserInfo(token: bad)
            )
            #expect(parsed == .invalidPayload)
        }
    }

    @Test("决策动作 sessionId 非法（非 v4 / 缺失）→ .invalidPayload")
    func parseDecisionRejectsBadSessionId() {
        let badId = NotificationActionHandler.parse(
            actionIdentifier: GateNotificationCategory.allowActionId,
            userInfo: Self.gateUserInfo(sessionId: "garbage")
        )
        #expect(badId == .invalidPayload)
        let missing = NotificationActionHandler.parse(
            actionIdentifier: GateNotificationCategory.denyActionId,
            userInfo: ["token": Self.token]
        )
        #expect(missing == .invalidPayload)
    }

    @Test("默认点按 + 合法 sessionId → .openSession（复用 DeepLinkRouter 同一解析）")
    func parseDefaultTapRoutes() throws {
        let parsed = NotificationActionHandler.parse(
            actionIdentifier: UNNotificationDefaultActionIdentifier,
            userInfo: Self.gateUserInfo(token: nil) // done 类通知无 token，点按仍可路由
        )
        let sessionId = try #require(UUID(uuidString: Self.sessionId))
        #expect(parsed == .openSession(sessionId: sessionId))
    }

    @Test("默认点按 + 非法 payload → .invalidPayload")
    func parseDefaultTapRejectsGarbage() {
        let parsed = NotificationActionHandler.parse(
            actionIdentifier: UNNotificationDefaultActionIdentifier,
            userInfo: ["sessionId": "'; DROP TABLE sessions;--"]
        )
        #expect(parsed == .invalidPayload)
    }

    @Test("dismiss / 未知动作 id → .dismissed（no-op）")
    func parseDismissAndUnknownActions() {
        #expect(NotificationActionHandler.parse(
            actionIdentifier: UNNotificationDismissActionIdentifier,
            userInfo: Self.gateUserInfo()
        ) == .dismissed)
        #expect(NotificationActionHandler.parse(
            actionIdentifier: "EVIL_ACTION", userInfo: Self.gateUserInfo()
        ) == .dismissed)
    }
}
