import UIKit
import XCTest
@testable import suixinkan

/// 极光推送测试，覆盖 payload、Registration ID 上报、生命周期和冷启动路由。
@MainActor
final class PushNotificationTests: XCTestCase {
    private var defaults: UserDefaults!
    private var appStore: AppStore!
    private var suiteName: String!

    override func setUp() {
        super.setUp()
        suiteName = "PushNotificationTests.\(UUID().uuidString)"
        defaults = UserDefaults(suiteName: suiteName)!
        defaults.removePersistentDomain(forName: suiteName)
        appStore = AppStore(defaults: defaults)
    }

    override func tearDown() {
        if let suiteName {
            defaults.removePersistentDomain(forName: suiteName)
        }
        defaults = nil
        appStore = nil
        suiteName = nil
        super.tearDown()
    }

    func testPayloadRoutesPaymentTypesToTheirPages() {
        XCTAssertEqual(
            PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination,
            .paymentRecord
        )
        XCTAssertEqual(
            PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination,
            .paymentDetails
        )
    }

    func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() {
        let conflictingFields: [AnyHashable: Any] = [
            "type": 2,
            "route": "payment",
            "uri": "/payment_qr",
            "action": "收款",
        ]

        [0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
            XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter)
        }
        XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter)
        XCTAssertEqual(PushPayload(userInfo: ["route": "payment"]).destination, .messageCenter)
        XCTAssertEqual(PushPayload(userInfo: ["data": ["type": 6]]).destination, .messageCenter)
        XCTAssertEqual(PushPayload(userInfo: ["type": 999]).destination, .messageCenter)
    }

    func testPayloadReadsTypeFromNestedJSONStringWithoutUsingOtherFields() {
        let nested = PushPayload(userInfo: [
            "extras": #"{"type":6,"route":"message_center"}"#,
        ])

        XCTAssertEqual(nested.destination, .paymentDetails)
    }

    func testPushAPIUsesAndroidCompatibleEndpointAndQuery() async throws {
        let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
        let api = PushAPI(client: APIClient(environment: .testing, session: session))

        try await api.registerJPushID("registration-id-123")

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "POST")
        XCTAssertEqual(request.url?.path, "/api/app/user/register-jpush-id")
        let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
        XCTAssertEqual(query?.first { $0.name == "jpush_reg_id" }?.value, "registration-id-123")
    }

    func testPrivacyGatePreventsSDKInitialization() {
        let sdk = PushSDKMock(registrationID: "reg-id")
        let manager = makeManager(sdk: sdk)

        manager.initializeIfPrivacyAccepted()
        XCTAssertEqual(sdk.initializeCount, 0)

        appStore.session.privacyAgreementAccepted = true
        manager.initializeIfPrivacyAccepted()
        manager.initializeIfPrivacyAccepted()
        XCTAssertEqual(sdk.initializeCount, 1)
    }

    func testEmptyRegistrationIDDoesNotUpload() async {
        authenticate(userID: "100")
        let sdk = PushSDKMock(registrationID: nil)
        let api = PushRegistrationAPIMock()
        let manager = makeManager(sdk: sdk, api: api)

        manager.handleLoginCompleted()
        await Task.yield()
        await Task.yield()

        XCTAssertTrue(api.registrationIDs.isEmpty)
    }

    func testLoggedOutStateDoesNotUpload() async {
        appStore.session.privacyAgreementAccepted = true
        let sdk = PushSDKMock(registrationID: "reg-id")
        let api = PushRegistrationAPIMock()
        let manager = makeManager(sdk: sdk, api: api)

        manager.initializeIfPrivacyAccepted()
        await Task.yield()
        await Task.yield()

        XCTAssertTrue(api.registrationIDs.isEmpty)
    }

    func testLoginRequestsAuthorizationAndUploadsRegistrationIDOnce() async {
        authenticate(userID: "100")
        let sdk = PushSDKMock(registrationID: "reg-id")
        let api = PushRegistrationAPIMock()
        let manager = makeManager(sdk: sdk, api: api)

        manager.handleLoginCompleted()
        await waitUntil { api.registrationIDs.count == 1 }
        manager.retryPendingRegistrationUpload()
        await Task.yield()

        XCTAssertEqual(sdk.initializeCount, 1)
        XCTAssertEqual(sdk.authorizationRequestCount, 1)
        XCTAssertEqual(api.registrationIDs, ["reg-id"])
    }

    func testAccountSwitchForcesRegistrationIDRebind() async {
        authenticate(userID: "100")
        let sdk = PushSDKMock(registrationID: "reg-id")
        let api = PushRegistrationAPIMock()
        let manager = makeManager(sdk: sdk, api: api)

        manager.handleLoginCompleted()
        await waitUntil { api.registrationIDs.count == 1 }
        appStore.session.userId = "200"
        manager.handleAccountSwitched()
        await waitUntil { api.registrationIDs.count == 2 }

        XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
    }

    func testFailedRegistrationUploadRetriesOnNextActivation() async {
        authenticate(userID: "100")
        let sdk = PushSDKMock(registrationID: "reg-id")
        let api = PushRegistrationAPIMock(errors: [PushTestError.expectedFailure, nil])
        let manager = makeManager(sdk: sdk, api: api)

        manager.handleLoginCompleted()
        await waitUntil { api.registrationIDs.count == 1 }
        await waitUntil { api.completedCallCount == 1 }
        manager.retryPendingRegistrationUpload()
        await waitUntil { api.registrationIDs.count == 2 }

        XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
    }

    func testNotificationTapRoutesButOrdinaryLifecycleDoesNot() {
        authenticate(userID: "100")
        let router = PushRouterMock()
        let manager = makeManager(router: router)

        manager.initializeIfPrivacyAccepted()
        XCTAssertTrue(router.destinations.isEmpty)

        manager.handleNotificationTap(
            payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]),
            requestIdentifier: "request-1"
        )
        XCTAssertEqual(router.destinations, [.paymentRecord])
    }

    func testRemoteNotificationNotifiesUnreadMessageStateChange() async {
        let manager = makeManager()
        let unreadChanged = expectation(
            forNotification: NotificationName.unreadMessageCountDidChange,
            object: nil
        )

        manager.handleRemoteNotification(["route": "message_center"])

        await fulfillment(of: [unreadChanged], timeout: 1)
    }

    func testApplicationIconBadgeUsesLatestNonnegativeCount() async {
        let badgeSetter = ApplicationIconBadgeSetterMock()
        let manager = makeManager(badgeSetter: badgeSetter)

        await manager.updateApplicationIconBadgeCount(12)
        await manager.updateApplicationIconBadgeCount(-2)

        XCTAssertEqual(badgeSetter.counts, [12, 0])
    }

    func testRouteCoordinatorWaitsForLoginAndDeduplicatesColdStartResponse() {
        let coordinator = PushRouteCoordinator(appStore: appStore)
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UINavigationController(rootViewController: UIViewController())
        coordinator.attach(window: window)

        coordinator.handle(destination: .paymentDetails, requestIdentifier: "cold-request")
        authenticate(userID: "100")
        let mainTab = MainTabBarController()
        mainTab.loadViewIfNeeded()
        window.rootViewController = mainTab
        coordinator.routePendingIfPossible()

        let navigationController = mainTab.selectedViewController as? UINavigationController
        XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)

        coordinator.handle(destination: .messageCenter, requestIdentifier: "cold-request")
        XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)
    }

    private func authenticate(userID: String) {
        appStore.session.privacyAgreementAccepted = true
        appStore.session.saveToken("token")
        appStore.session.userId = userID
        appStore.session.accountType = .storeUser
    }

    private func makeManager(
        sdk: PushSDKMock? = nil,
        api: PushRegistrationAPIMock? = nil,
        router: PushRouterMock? = nil,
        badgeSetter: ApplicationIconBadgeSetterMock? = nil
    ) -> PushNotificationManager {
        let resolvedSDK: PushSDKMock
        if let sdk {
            resolvedSDK = sdk
        } else {
            resolvedSDK = PushSDKMock(registrationID: nil)
        }

        let resolvedAPI: PushRegistrationAPIMock
        if let api {
            resolvedAPI = api
        } else {
            resolvedAPI = PushRegistrationAPIMock()
        }

        let resolvedRouter: PushRouterMock
        if let router {
            resolvedRouter = router
        } else {
            resolvedRouter = PushRouterMock()
        }

        return PushNotificationManager(
            sdk: resolvedSDK,
            api: resolvedAPI,
            appStore: appStore,
            defaults: defaults,
            router: resolvedRouter,
            applicationIconBadgeSetter: badgeSetter ?? ApplicationIconBadgeSetterMock()
        )
    }

    private func waitUntil(
        timeoutIterations: Int = 100,
        _ condition: () -> Bool
    ) async {
        for _ in 0..<timeoutIterations {
            if condition() { return }
            await Task.yield()
        }
        XCTFail("等待异步条件超时")
    }
}

/// 记录 App 桌面图标角标更新的测试替身。
@MainActor
private final class ApplicationIconBadgeSetterMock: ApplicationIconBadgeSetting {
    private(set) var counts: [Int] = []

    func setBadgeCount(_ count: Int) async {
        counts.append(count)
    }
}

/// 可控的极光 SDK 测试替身，记录初始化与通知授权行为。
@MainActor
private final class PushSDKMock: PushSDKProviding {
    private let registrationID: String?
    private(set) var initializeCount = 0
    private(set) var authorizationRequestCount = 0
    private(set) var deviceTokens: [Data] = []

    init(registrationID: String?) {
        self.registrationID = registrationID
    }

    func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
        initializeCount += 1
    }

    func requestAuthorization(delegate: JPUSHRegisterDelegate) {
        authorizationRequestCount += 1
    }

    func registerDeviceToken(_ deviceToken: Data) {
        deviceTokens.append(deviceToken)
    }

    func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
        completion(0, registrationID)
    }
}

/// 可控制成功或失败结果的 Registration ID 上报接口测试替身。
@MainActor
private final class PushRegistrationAPIMock: PushRegistrationServing {
    private var errors: [Error?]
    private(set) var registrationIDs: [String] = []
    private(set) var completedCallCount = 0

    init(errors: [Error?] = []) {
        self.errors = errors
    }

    func registerJPushID(_ registrationID: String) async throws {
        registrationIDs.append(registrationID)
        let error = errors.isEmpty ? nil : errors.removeFirst()
        completedCallCount += 1
        if let error { throw error }
    }
}

/// 记录通知点击路由行为的测试替身。
@MainActor
private final class PushRouterMock: PushRouting {
    private(set) var destinations: [PushDestination] = []
    private(set) var resetCount = 0

    func attach(window: UIWindow) {}

    func handle(destination: PushDestination, requestIdentifier: String) {
        destinations.append(destination)
    }

    func routePendingIfPossible() {}

    func resetPendingRoute() {
        resetCount += 1
    }
}

/// 推送单元测试使用的可预期失败错误。
private enum PushTestError: Error {
    case expectedFailure
}
