@testable import AnalyticsClient
@testable import APIClient
@testable import AppSessionCountClient
@testable import AttributionAndDeeplinkingClient
@testable import BrazeClient
import ClerkClient
import ComposableArchitecture
import Dependencies
@testable import DebugFeatureClient
@testable import FirebaseClient
import Foundation
@testable import InAppNotificationClient
@testable import PaywallClient
@testable import RageshakeClient
@testable import StatsigClient
@testable import SunoModelClient
import Testing
@testable import UserSessionTransitionClient

@Suite("UserSessionTransitionClient Tests", .serialized)
struct UserSessionTransitionClientTests {
    // MARK: - Transition Kind Tests

    @Suite("UserTransitionKind Tests")
    struct UserTransitionKindTests {
        @Test("Logged in transition")
        func testLoggedInTransition() {
            let kind = UserSessionTransitionClient.UserTransitionKind(
                fromUserId: nil,
                toUserId: "user123"
            )
            #expect(kind == .loggedIn)
        }

        @Test("Logged out transition")
        func testLoggedOutTransition() {
            let kind = UserSessionTransitionClient.UserTransitionKind(
                fromUserId: "user123",
                toUserId: nil
            )
            #expect(kind == .loggedOut)
        }

        @Test("Account switch transition")
        func testAccountSwitchTransition() {
            let kind = UserSessionTransitionClient.UserTransitionKind(
                fromUserId: "user123",
                toUserId: "user456"
            )
            #expect(kind == .accountSwitch)
        }

        @Test("No-op transition - same user")
        func testNoOpTransitionSameUser() {
            let kind = UserSessionTransitionClient.UserTransitionKind(
                fromUserId: "user123",
                toUserId: "user123"
            )
            #expect(kind == .noOp)
        }

        @Test("No-op transition - both nil")
        func testNoOpTransitionBothNil() {
            let kind = UserSessionTransitionClient.UserTransitionKind(
                fromUserId: nil,
                toUserId: nil
            )
            #expect(kind == .noOp)
        }
    }

    // MARK: - ServiceUserState Tests

    @Suite("ServiceUserState Tests")
    struct ServiceUserStateTests {
        @Test("Service user state with authenticated user")
        func testServiceUserStateWithAuthenticatedUser() {
            let user = User.mock()
            let me = Me.mock(user: user)
            let userState = UserSessionTransitionClient.ServiceUserState(
                me: me,
                clerkUser: .mock(userId: me.user.id)
            )
            #expect(userState.allHave(id: user.id))
        }

        @Test("Service user state with anonymous user")
        func testServiceUserStateWithAnonymousUser() {
            let userState = UserSessionTransitionClient.ServiceUserState(me: nil, clerkUser: nil)
            #expect(userState.allAnonymous)
        }
    }

    // MARK: - User Transition Tests

    @Suite("User Transition Tests", .serialized)
    struct UserTransitionTests {
        @Test("Login transition")
        func testLoginTransition() async throws {
            let user = User.mock()
            let me = Me.mock(user: user, models: [.mock()])

            let (client, captures) = createClientWithCaptures(
                clerkUserId: me.user.id,
                clerkEmail: user.email
            )

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            let result = try await client.performUserTransition(transition)

            // Verify result
            #expect(result.kind == .loggedIn)
            #expect(result.configuredModels == true)

            // Verify analytics user was set
            #expect(captures.analyticsUser.value == .authenticated(me: me))

            // Verify attribution user was set
            #expect(captures.attributionUserId.value == user.id)

            // Verify paywall user was set
            #expect(captures.paywallUser.value == .authenticated(userId: user.id))

            // Verify braze user was set
            #expect(captures.brazeUser.value == .authenticated(userId: user.id))

            // Verify statsig user was set

            #expect(captures.statsigUser.value == .authenticated(userId: user.id, email: user.email, custom: nil, customIDs: nil))

            // Verify rageshake user was set
            switch captures.rageshakeUser.value {
            case .authenticated(let id, let email, let displayName, let handle):
                #expect(id == user.id)
                #expect(email == user.email)
                #expect(displayName == user.displayName)
                #expect(handle == user.handle)

            default:
                Issue.record("Expected authenticated rageshake user")
            }

            // Verify firebase user was set
            #expect(captures.firebaseUser.value == .authenticated(userId: user.id, email: user.email))

            // Verify models were configured
            #expect(captures.modelsConfigured.value == true)

            // Verify app session transition was tracked
            #expect(captures.appSessionTransition.value?.0 == .unauthenticated)
            #expect(captures.appSessionTransition.value?.1 == .authenticated(userId: user.id))
        }

        @Test("Logout transition")
        func testLogoutTransition() async throws {
            let user = User.mock()
            let me = Me.mock(user: user)
            let (client, captures) = createClientWithCaptures(clerkUserId: me.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: me,
                to: nil
            )

            let result = try await client.performUserTransition(transition)

            // Verify result
            #expect(result.kind == .loggedOut)
            #expect(result.configuredModels == false)

            // Verify analytics was reset
            #expect(captures.analyticsResetCalled.value == true)

            // Verify notifications were cleared
            #expect(captures.notificationsClearedCalled.value == true)

            // Verify app session transition was tracked
            #expect(captures.appSessionTransition.value?.0 == .authenticated(userId: user.id))
            #expect(captures.appSessionTransition.value?.1 == .unauthenticated)
        }

        @Test("Account switch transition")
        func testAccountSwitchTransition() async throws {
            let fromUser = User.mock(id: "user1")
            let toUser = User.mock(id: "user2")
            let fromMe = Me.mock(user: fromUser)
            let toMe = Me.mock(user: toUser, models: [.mock()])

            let (client, captures) = createClientWithCaptures(clerkUserId: toMe.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: fromMe,
                to: toMe
            )

            let result = try await client.performUserTransition(transition)

            // Verify result
            #expect(result.kind == .accountSwitch)
            #expect(result.configuredModels == true)

            // Verify analytics user was set to new user
            #expect(captures.analyticsUser.value == .authenticated(me: toMe))

            // Verify models were configured
            #expect(captures.modelsConfigured.value == true)

            // Verify app session transition was tracked
            #expect(captures.appSessionTransition.value?.0 == .authenticated(userId: fromUser.id))
            #expect(captures.appSessionTransition.value?.1 == .authenticated(userId: toUser.id))
        }

        @Test("No-op transition")
        func testNoOpTransition() async throws {
            let user = User.mock()
            let me = Me.mock(user: user, models: [.mock()])

            let (client, captures) = createClientWithCaptures(clerkUserId: me.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: me,
                to: me
            )

            let result = try await client.performUserTransition(transition)

            // Verify result
            #expect(result.kind == .noOp)
            #expect(result.configuredModels == true)

            // Verify app session transition was tracked (same user, so no change)
            #expect(captures.appSessionTransition.value?.0 == .authenticated(userId: user.id))
            #expect(captures.appSessionTransition.value?.1 == .authenticated(userId: user.id))
        }

        @Test("Model configuration error handling")
        func testModelConfigurationErrorHandling() async throws {
            let user = User.mock()
            let me = Me.mock(user: user, models: [.mock()])
            let (client, _) = createClientWithErrorHandling(getBillingInfoError: true, clerkUserId: me.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            let result = try await client.performUserTransition(transition)

            // Verify result - getBillingInfo error is handled gracefully
            #expect(result.kind == .loggedIn)
            #expect(result.configuredModels == false)
        }

        @Test("Statsig error handling")
        func testStatsigErrorHandling() async throws {
            let me = Me.mock(user: .mock(), models: [.mock()])

            let (client, captures) = createClientWithErrorHandling(statsigError: true, clerkUserId: me.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            // Should not throw despite Statsig error
            let result = try await client.performUserTransition(transition)

            // Verify result - transition should still succeed
            #expect(result.kind == .loggedIn)
            #expect(result.configuredModels == true)
            #expect(captures.statsigUser.value != nil)
        }

        @Test("Statsig user uses me.user.email as email, not clerkUser.email")
        func testStatsigUserUsesCorrectEmail() async throws {
            let sunoUserId = "suno-user-123"
            let sunoEmail = "suno@example.com"
            let clerkUserId = "user_clerk_stable_id_456"
            let clerkEmail = "clerk@example.com"
            let user = User(
                id: sunoUserId,
                handle: "testuser",
                email: sunoEmail,
                username: "testuser",
                isHandleUpdated: true
            )
            let me = Me.mock(user: user, models: [.mock()])

            let (client, captures) = createClientWithCaptures(
                clerkUserId: clerkUserId,
                clerkEmail: clerkEmail
            )

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            _ = try await client.performUserTransition(transition)

            switch captures.statsigUser.value {
            case .authenticated(let userId, let email, _, _):
                #expect(userId == sunoUserId)
                #expect(userId == me.user.id)
                #expect(userId != clerkUserId)
                #expect(email == user.email)
                #expect(email == me.user.email)
                #expect(email == sunoEmail)
                #expect(email != clerkEmail)

            default:
                Issue.record("Expected authenticated Statsig user")
            }
        }

        @Test("Statsig user uses me.user.id as userId, not clerkUserId")
        func testStatsigUserUsesCorrectUserId() async throws {
            let sunoUserId = "suno-user-123"
            let clerkUserId = "user_clerk_stable_id_456"
            let user = User.mock(id: sunoUserId)
            let me = Me.mock(user: user, models: [.mock()])

            let (client, captures) = createClientWithCaptures(
                clerkUserId: clerkUserId,
                clerkEmail: user.email
            )

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            _ = try await client.performUserTransition(transition)

            switch captures.statsigUser.value {
            case .authenticated(let userId, _, _, _):
                #expect(userId == sunoUserId)
                #expect(userId == me.user.id)
                #expect(userId != clerkUserId)
                #expect(userId.hasPrefix("user_") == false)
                #expect(clerkUserId.hasPrefix("user_") == true)

            default:
                Issue.record("Expected authenticated Statsig user")
            }
        }

        @Test("Statsig user preserves customIDs from me.statsigCustomProperties")
        func testStatsigUserPreservesCustomIDs() async throws {
            let sunoUserId = "suno-user-789"
            let clerkUserId = "user_clerk_stable_id_999"
            let user = User.mock(id: sunoUserId)
            let me = Me.mock(
                user: user,
                models: [.mock()],
                statsigCustomProperties: StatsigCustomProperties(
                    custom: ["plan": "premium"],
                    customIDs: ["externalID": "external-123"]
                )
            )

            let (client, captures) = createClientWithCaptures(
                clerkUserId: clerkUserId,
                clerkEmail: user.email
            )

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            _ = try await client.performUserTransition(transition)

            switch captures.statsigUser.value {
            case .authenticated(let userId, let email, let custom, let customIDs):
                #expect(userId == sunoUserId)
                #expect(userId == me.user.id)
                #expect(userId != clerkUserId)
                #expect(userId.hasPrefix("user_") == false)
                #expect(clerkUserId.hasPrefix("user_") == true)
                #expect(email == user.email)
                #expect(custom == ["plan": "premium"])
                #expect(customIDs?["externalID"] == "external-123")

            default:
                Issue.record("Expected authenticated Statsig user")
            }
        }
    }

    // MARK: - Current Service User State Tests

    @Suite("Current Service User State Tests", .serialized)
    struct CurrentServiceUserStateTests {
        @Test("Get current service user state after transition")
        func testGetCurrentServiceUserStateAfterTransition() async throws {
            let user = User.mock()
            let me = Me.mock(user: user)
            let (client, _) = createClientWithCaptures(clerkUserId: me.user.id)

            let transition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )

            _ = try await client.performUserTransition(transition)

            // Should now have service user state
            let currentServiceUserState = await client.getServiceUserState()
            #expect(currentServiceUserState != nil)

            // Verify the service user state matches what we expect
            #expect(currentServiceUserState?.allHave(id: user.id) ?? false)
        }

        @Test("Get current service user state after logout")
        func testGetCurrentServiceUserStateAfterLogout() async throws {
            let me = Me.mock()
            let (client, _) = createClientWithCaptures(clerkUserId: me.user.id)

            // First login
            let loginTransition = UserSessionTransitionClient.UserTransition(
                from: nil,
                to: me
            )
            _ = try await client.performUserTransition(loginTransition)

            // Then logout
            let logoutTransition = UserSessionTransitionClient.UserTransition(
                from: me,
                to: nil
            )
            _ = try await client.performUserTransition(logoutTransition)

            // Service user state should now be anonymous
            let currentUserState = await client.getServiceUserState()
            #expect(currentUserState?.allAnonymous ?? false)
        }
    }
}

// MARK: - Dependency Helpers

private extension UserSessionTransitionClientTests {
    static func createClientWithDependencies(
        _ configure: (inout DependencyValues) -> Void
    ) -> UserSessionTransitionClient {
        withDependencies {
            setDefaultDependencies(&$0)
            configure(&$0)
        } operation: {
            let actor = UserSessionTransitionActor()

            return UserSessionTransitionClient(
                performUserTransition: { transition in
                    try await actor.performUserTransition(transition)
                },
                getServiceUserState: {
                    await actor.getServiceUserState()
                }
            )
        }
    }

    static func setDefaultDependencies(
        _ dependencies: inout DependencyValues
    ) {
        dependencies.analyticsClient.identify = { _ in }
        dependencies.analyticsClient.resetIdentity = {}
        dependencies[AttributionAndDeeplinkingClient.self].setUser = { _ in }
        dependencies[PaywallClient.self].updateUser = { _ in }
        dependencies[BrazeClient.self].setUser = { _ in }
        dependencies[StatsigClient.self].setUser = { _ in }
        dependencies[DebugFeatureClient.self].setRageshakeUser = { _ in }
        dependencies[DebugFeatureClient.self].setRageshakeEnabled = { _ in }
        dependencies[DebugFeatureClient.self].isDebugMenuAvailable = { false }
        dependencies.firebaseClient.setUser = { _ in }
        dependencies[SunoModelClient.self].configure = { _, _, _, _ in }
        dependencies.apiClientV2.getBillingInfo = { .mock(accessibleFeatures: []) }
        dependencies.appSessionCountClient.trackAuthenticationTransition = { _, _ in }
        dependencies.inAppNotificationClient.clearNotificationsInPersistentStore = {}
    }
}

// MARK: - Test Helpers

private extension UserSessionTransitionClientTests {
    struct TestCaptures {
        let analyticsUser = LockIsolated<AnalyticsClient.User?>(nil)
        let attributionUserId = LockIsolated<String?>(nil)
        let paywallUser = LockIsolated<PaywallClient.User?>(nil)
        let brazeUser = LockIsolated<BrazeClient.User?>(nil)
        let statsigUser = LockIsolated<StatsigClient.User?>(nil)
        let rageshakeUser = LockIsolated<DebugRageshakeUser?>(nil)
        let firebaseUser = LockIsolated<FirebaseClient.User?>(nil)
        let appSessionTransition = LockIsolated<(SessionAuthenticationState, SessionAuthenticationState)?>(nil)
        let analyticsResetCalled = LockIsolated<Bool>(false)
        let notificationsClearedCalled = LockIsolated<Bool>(false)
        let modelsConfigured = LockIsolated<Bool>(false)
    }

    static func setupCapturingDependencies(
        _ dependencies: inout DependencyValues,
        captures: TestCaptures,
        clerkUserId: String,
        clerkEmail: String?
    ) {
        dependencies.analyticsClient.identify = { user in
            captures.analyticsUser.withValue { $0 = user }
        }
        dependencies.analyticsClient.resetIdentity = {
            captures.analyticsResetCalled.withValue { $0 = true }
        }
        dependencies[AttributionAndDeeplinkingClient.self].setUser = { user in
            captures.attributionUserId.withValue {
                $0 = switch user {
                case .authenticated(let userId): userId
                case .anonymous: nil
                }
            }
        }
        dependencies[PaywallClient.self].updateUser = { user in
            captures.paywallUser.withValue { $0 = user }
        }
        dependencies[BrazeClient.self].setUser = { user in
            captures.brazeUser.withValue { $0 = user }
        }
        dependencies[StatsigClient.self].setUser = { user in
            captures.statsigUser.withValue { $0 = user }
        }
        dependencies[DebugFeatureClient.self].setRageshakeUser = { user in
            captures.rageshakeUser.withValue { $0 = user }
        }
        dependencies.firebaseClient.setUser = { user in
            captures.firebaseUser.withValue { $0 = user }
        }
        dependencies[SunoModelClient.self].configure = { _, _, _, _ in
            captures.modelsConfigured.withValue { $0 = true }
        }
        dependencies.appSessionCountClient.trackAuthenticationTransition = { from, to in
            captures.appSessionTransition.withValue { $0 = (from, to) }
        }
        dependencies.inAppNotificationClient.clearNotificationsInPersistentStore = {
            captures.notificationsClearedCalled.withValue { $0 = true }
        }
        dependencies[ClerkClient.self].user = {
            .mock(userId: clerkUserId, email: clerkEmail)
        }
    }

    static func createClientWithCaptures(
        clerkUserId: String,
        clerkEmail: String? = nil
    ) -> (UserSessionTransitionClient, TestCaptures) {
        let captures = TestCaptures()
        let client = createClientWithDependencies { dependencies in
            setupCapturingDependencies(
                &dependencies,
                captures: captures,
                clerkUserId: clerkUserId,
                clerkEmail: clerkEmail
            )
        }
        return (client, captures)
    }

    static func createClientWithErrorHandling(
        getBillingInfoError: Bool = false,
        statsigError: Bool = false,
        clerkUserId: String,
        clerkEmail: String? = nil
    ) -> (UserSessionTransitionClient, TestCaptures) {
        let captures = TestCaptures()
        let client = createClientWithDependencies { dependencies in
            setupCapturingDependencies(
                &dependencies,
                captures: captures,
                clerkUserId: clerkUserId,
                clerkEmail: clerkEmail
            )

            if getBillingInfoError {
                dependencies.apiClientV2.getBillingInfo = {
                    throw NSError.mock()
                }
            }

            if statsigError {
                dependencies[StatsigClient.self].setUser = { user in
                    captures.statsigUser.withValue { $0 = user }
                    throw NSError.mock()
                }
            }
        }
        return (client, captures)
    }
}

// MARK: - Mocks

private extension Me {
    static func mock(
        user: User? = nil,
        models: [SunoModel] = [],
        statsigCustomProperties: StatsigCustomProperties? = nil
    ) -> Self {
        let defaultUser = user ?? User.mock()
        return Me(
            models: models,
            roles: [:],
            flags: [:],
            user: defaultUser,
            statsigCustomProperties: statsigCustomProperties
        )
    }
}

private extension SunoModel {
    static func mock(
        id: String = "model-123",
        name: String = "v4",
        majorVersion: Int = 4,
        externalKey: String = "chirp-v4-tau",
        canUse: Bool = true
    ) -> SunoModel {
        SunoModel(
            id: id,
            name: name,
            majorVersion: majorVersion,
            externalKey: externalKey,
            description: "Test model",
            canUse: canUse,
            maxLengths: nil
        )
    }
}

private extension NSError {
    static func mock() -> Error {
        NSError(domain: "UserTransitionClientTests", code: -1)
    }
}

private extension ClerkUser {
    static func mock(
        userId: String,
        email: String? = nil
    ) -> Self {
        .init(
            backupCodeEnabled: false,
            createdAt: .now,
            createOrganizationEnabled: false,
            deleteSelfEnabled: false,
            emailAddresses: {
                if let email {
                    [.init(id: userId, emailAddress: email)]
                } else {
                    []
                }
            }(),
            externalAccounts: [],
            hasImage: false,
            id: userId,
            imageUrl: "",
            organizationMemberships: [],
            passkeys: [],
            passwordEnabled: false,
            phoneNumbers: [],
            totpEnabled: false,
            twoFactorEnabled: false,
            updatedAt: .now
        )
    }
}
