import AnalyticsClient
@testable import APIClient
import AppSessionCountClient
import ComposableArchitecture
@testable import FeatureApp
import Foundation
import Testing
import Utilities

import BrazeClient
import ClerkClient
import FeatureOnboarding
import FeatureRoot
import Localization
import PaywallClient
import RageshakeClient
import UserNotificationsClient
import UserSessionTransitionClient

@MainActor
struct AppCoordinatorTests {
    @MainActor
    struct LoggedOut {
        @Test
        func testExpectNoUserSessionChanged() async {
            let store = baseStore(withMoreDependencies: {
                $0[ClerkClient.self].isLoggedIn = { @Sendable in false }
            })

            // Get to the auth screen
            await store.send(.onAppear)

            // Expect to be logged out, and sent to the welcome screen
            await store.receive(\.rootView, .network(.failedToFetch)) {
                $0.destination = .welcome(WelcomeCoordinator.State())
            }
        }
    }

    @MainActor
    struct Auth {
        @Test
        func testAuthSideEffectsWhenSignedOutExpectOneSignIn() async {
            let store = baseStore(withMoreDependencies: signedInDependencies)

            // Expecting userSessionChanged to be called only 1 time
            await confirmation(expectedCount: 1) { confirmation in
                store.dependencies[AnalyticsClient.self].identify = { @Sendable user in
                    switch user {
                    case .anonymous:
                        Issue.record("Expected authenticated analytics user")

                    case .authenticated(me: let authenticatedMe):
                        #expect(authenticatedMe == me)
                        // Confirm that we set the analytics user ID during sign in
                        confirmation.confirm()
                    }
                }

                // Get to the auth screen
                await store.send(.onAppear)
                await store.receive(\.rootView, .network(.failedToFetch)) {
                    $0.previousMe = nil
                }

                await store.send(.destination(.presented(.welcome(.delegate(.onboardingV1(.signUpResult(.success(me))))))))
                await store.receive(\.rootView, .userAction(.performedAuth(me, isFTUX: false)))
            }
        }

        @Test
        func testResumptionExpectOneUserSessionChange() async {
            let nonOnboardedUser = me.with {
                $0.user.isHandleUpdated = false
            }

            let store = baseStore(withMoreDependencies: signedInDependencies)
            store.dependencies[ClerkClient.self].isLoggedIn = { @Sendable in true }
            store.dependencies[APIClientV2.self].getMe = { @Sendable in nonOnboardedUser }

            await confirmation(expectedCount: 1) { confirmation in
                store.dependencies[AnalyticsClient.self].identify = { @Sendable user in
                    switch user {
                    case .anonymous:
                        Issue.record("Expected authenticated analytics user")

                    case .authenticated(me: let authenticatedMe):
                        #expect(authenticatedMe == nonOnboardedUser)
                        // Confirm that we set the analytics user ID during sign in
                        confirmation.confirm()
                    }
                }

                // Get to the auth screen
                await store.send(.onAppear)

                /// exiting this closure will enforce that we've identified the user once to Analytics and other 3rd party SDKs through `AppCoordinator.userSessionChanged(_:state:)
            }

            // Expect to be authenticated and sent to loggedInV1
            await store.receive(\.rootView, .network(.fetched(nonOnboardedUser, isFTUX: false))) {
                $0.previousMe = nonOnboardedUser
            }
        }
    }

    @MainActor
    struct LoggedIn {
        @Test
        func testOpenAppExpectOneUserSessionChanged() async {
            let store = baseStore(withMoreDependencies: signedInDependencies)
            store.dependencies[ClerkClient.self].isLoggedIn = { @Sendable in true }
            store.dependencies[APIClientV2.self].getMe = { @Sendable in me }

            await confirmation(expectedCount: 1) { confirmation in
                store.dependencies[AnalyticsClient.self].identify = { @Sendable user in
                    switch user {
                    case .anonymous:
                        Issue.record("Expected authenticated analytics user")

                    case .authenticated(me: let authenticatedMe):
                        #expect(authenticatedMe == me)
                        // Confirm that we set the analytics user ID during sign in
                        confirmation.confirm()
                    }
                }

                // Get to the auth screen
                await store.send(.onAppear)
            }

            // Expect to be authenticated, and sent home
            await store.receive(\.rootView, .network(.fetched(me, isFTUX: false))) {
                $0.previousMe = me
            }
        }
    }
}

// MARK: mocks

private func baseStore(
    withMoreDependencies: (inout DependencyValues) -> Void = { _ in }
) -> TestStore<AppCoordinator.State, AppCoordinator.Action> {
    TestStore(
        initialState: .init(),
        reducer: { AppCoordinator() },
        withDependencies: {
            /// onAppear
            $0[APIClient.self].manifest = { @Sendable in Manifest(warnVersion: nil, forceVersion: nil) }
            $0[NetworkMonitor.self].start = {}
            $0[AnalyticsClient.self].anonymousId = { "123" }

            withMoreDependencies(&$0)
        }
    ).with {
        $0.exhaustivity = .off
    }
}

private let me = Me(models: [], roles: [:], flags: [:], user: User.mock())

// When a user signs in, the AppCoordinator accesses the following dependencies
private let signedInDependencies: (inout DependencyValues) -> Void = {
    $0[ClerkClient.self].isLoggedIn = { @Sendable in false }

    $0[APIClient.self].redeemPromoCode = { @Sendable _ in
        throw NSError(domain: "Not implemented", code: 1, userInfo: nil)
    }
    $0[APIClientV2.self].getBillingInfo = { @Sendable in
        throw NSError(domain: "Not implemented", code: 1, userInfo: nil)
    }
    $0[RageshakeClient.self].setIsEnabled = { _ in }
    $0[RageshakeClient.self].setUser = { _ in }
    $0[PaywallClient.self].updateUser = { _ in }
    $0[BrazeClient.self].configure = {}
    $0[BrazeClient.self].setUser = { _ in }
    $0[UserNotificationClient.self].getNotificationSettings = { @Sendable in .init(authorizationStatus: .denied) }
    $0[AppSessionCountClient.self] = .liveValue
    $0.userSessionTransitionClient = .testValue
}

extension TestStore: Then {}
