//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//

import AnalyticsEvents
@testable import ElementX
import PostHog
import XCTest

class AnalyticsTests: XCTestCase {
    private var appSettings: AppSettings!
    private var analyticsClient: AnalyticsClientMock!
    private var posthogMock: PHGPostHogMock!
    
    override func setUp() {
        AppSettings.resetAllSettings()
        appSettings = AppSettings()
        
        analyticsClient = AnalyticsClientMock()
        analyticsClient.isRunning = false
        ServiceLocator.shared.register(analytics: AnalyticsService(client: analyticsClient,
                                                                   appSettings: appSettings))
        
        posthogMock = PHGPostHogMock()
        posthogMock.configureMockBehavior()
    }
    
    override func tearDown() {
        AppSettings.resetAllSettings()
    }
    
    func testAnalyticsPromptNewUser() {
        // Given a fresh install of the app (without PostHog analytics having been set).
        // When the user is prompted for analytics.
        let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
        
        // Then the prompt should be shown.
        XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
    }
    
    func testAnalyticsPromptUserDeclinedPostHog() {
        // Given an existing install of the app where the user previously declined PostHog
        appSettings.analyticsConsentState = .optedOut
        
        // When the user is prompted for analytics
        let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
        
        // Then no prompt should be shown.
        XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
    }
    
    func testAnalyticsPromptUserAcceptedPostHog() {
        // Given an existing install of the app where the user previously accepted PostHog
        appSettings.analyticsConsentState = .optedIn
        
        // When the user is prompted for analytics
        let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
        
        // Then no prompt should be shown.
        XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
    }
    
    func testAnalyticsPromptNotDisplayed() {
        // Given a fresh install of the app Analytics should be disabled
        XCTAssertEqual(appSettings.analyticsConsentState, .unknown)
        XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
        XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled)
    }
    
    func testAnalyticsOptOut() {
        // Given a fresh install of the app (without PostHog analytics having been set).
        // When analytics is opt-out
        ServiceLocator.shared.analytics.optOut()
        // Then analytics should be disabled
        XCTAssertEqual(appSettings.analyticsConsentState, .optedOut)
        XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
        XCTAssertFalse(analyticsClient.isRunning)
        // Analytics client should have been stopped
        XCTAssertTrue(analyticsClient.stopCalled)
    }
    
    func testAnalyticsOptIn() {
        // Given a fresh install of the app (without PostHog analytics having been set).
        // When analytics is opt-in
        ServiceLocator.shared.analytics.optIn()
        // The analytics should be enabled
        XCTAssertEqual(appSettings.analyticsConsentState, .optedIn)
        XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled)
        // Analytics client should have been started
        XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled)
    }
    
    func testAnalyticsStartIfNotEnabled() {
        // Given an existing install of the app where the user previously declined the tracking
        appSettings.analyticsConsentState = .optedOut
        // Analytics should not start
        XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
        ServiceLocator.shared.analytics.startIfEnabled()
        XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled)
    }
    
    func testAnalyticsStartIfEnabled() {
        // Given an existing install of the app where the user previously accepted the tracking
        appSettings.analyticsConsentState = .optedIn
        // Analytics should start
        XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled)
        ServiceLocator.shared.analytics.startIfEnabled()
        XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled)
    }
    
    func testAddingUserProperties() {
        // Given a client with no user properties set
        let client = PostHogAnalyticsClient()
        XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")
        
        // When updating the user properties
        client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil,
                                                                  ftueUseCaseSelection: .PersonalMessaging,
                                                                  numFavouriteRooms: 4,
                                                                  numSpaces: 5, recoveryState: .Disabled, verificationState: .Verified))
        
        // Then the properties should be cached
        XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
        XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
        XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should match.")
        XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.")
        XCTAssertEqual(client.pendingUserProperties?.verificationState, AnalyticsEvent.UserProperties.VerificationState.Verified, "The verification state should match.")
        XCTAssertEqual(client.pendingUserProperties?.recoveryState, AnalyticsEvent.UserProperties.RecoveryState.Disabled, "The recovery state should match.")
    }
    
    func testMergingUserProperties() {
        // Given a client with a cached use case user properties
        let client = PostHogAnalyticsClient()
        client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
                                                                  numFavouriteRooms: nil,
                                                                  numSpaces: nil, recoveryState: nil, verificationState: nil))
        
        XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
        XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
        XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.")
        XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")
        
        // When updating the number of spaced
        client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil,
                                                                  numFavouriteRooms: 4,
                                                                  numSpaces: 5, recoveryState: nil, verificationState: nil))
        
        // Then the new properties should be updated and the existing properties should remain unchanged
        XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
        XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.")
        XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should have been updated.")
        XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
    }
    
    func testSendingUserProperties() {
        // Given a client with user properties set
        
        let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
        client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
        
        client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
                                                                  numFavouriteRooms: nil,
                                                                  numSpaces: nil, recoveryState: nil, verificationState: nil))
        
        XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
        XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
        
        // When sending an event (tests run under Debug configuration so this is sent to the development instance)
        let someEvent = AnalyticsEvent.Error(context: nil,
                                             cryptoModule: .Rust,
                                             cryptoSDK: .Rust,
                                             domain: .E2EE,
                                             eventLocalAgeMillis: nil,
                                             isFederated: nil,
                                             isMatrixDotOrg: nil,
                                             name: .OlmKeysNotSentError,
                                             timeToDecryptMillis: nil,
                                             userTrustsOwnIdentity: nil,
                                             wasVisibleToUser: nil)
        client.capture(someEvent)
        
        let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
        
        // The user properties should have been added
        XCTAssertEqual(capturedEvent?.userProperties?["ftueUseCaseSelection"] as? String, AnalyticsEvent.UserProperties.FtueUseCaseSelection.PersonalMessaging.rawValue)
        
        // Then the properties should be cleared
        XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
    }
    
    func testResetConsentState() {
        // Given an existing install of the app where the user previously accpeted the tracking
        appSettings.analyticsConsentState = .optedIn
        XCTAssertFalse(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
        
        // When forgetting analytics consents
        ServiceLocator.shared.analytics.resetConsentState()
        
        // Then the analytics prompt should be presented again
        XCTAssertEqual(appSettings.analyticsConsentState, .unknown)
        XCTAssertTrue(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
    }
    
    func testSendingAndUpdatingSuperProperties() {
        // Given a client with user properties set
        let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
        client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
        
        client.updateSuperProperties(
            AnalyticsEvent.SuperProperties(appPlatform: .EXI,
                                           cryptoSDK: .Rust,
                                           cryptoSDKVersion: "000")
        )
        
        // When sending an event (tests run under Debug configuration so this is sent to the development instance)
        client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home))
        
        let screenEvent = posthogMock.screenPropertiesReceivedArguments
        
        XCTAssertEqual(screenEvent?.screenTitle, AnalyticsEvent.MobileScreen.ScreenName.Home.rawValue)
        
        // All the super properties should have been added
        XCTAssertEqual(screenEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
        XCTAssertEqual(screenEvent?.properties?["appPlatform"] as? String, "EXI")
        XCTAssertEqual(screenEvent?.properties?["cryptoSDKVersion"] as? String, "000")
        
        // It should be the same for any event
        let someEvent = AnalyticsEvent.Error(context: nil,
                                             cryptoModule: .Rust,
                                             cryptoSDK: .Rust,
                                             domain: .E2EE,
                                             eventLocalAgeMillis: nil,
                                             isFederated: nil,
                                             isMatrixDotOrg: nil,
                                             name: .OlmKeysNotSentError,
                                             timeToDecryptMillis: nil,
                                             userTrustsOwnIdentity: nil,
                                             wasVisibleToUser: nil)
        client.capture(someEvent)
        
        let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
        
        // All the super properties should have been added
        XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
        XCTAssertEqual(capturedEvent?.properties?["appPlatform"] as? String, "EXI")
        XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "000")
        
        // Updating should keep the previously set properties
        client.updateSuperProperties(
            AnalyticsEvent.SuperProperties(appPlatform: .EXI,
                                           cryptoSDK: .Rust,
                                           cryptoSDKVersion: "001")
        )
        
        client.capture(someEvent)
        let capturedEvent2 = posthogMock.capturePropertiesUserPropertiesReceivedArguments
        
        // All the super properties should have been added, with the one udpated
        XCTAssertEqual(capturedEvent2?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
        XCTAssertEqual(capturedEvent2?.properties?["appPlatform"] as? String, "EXI")
        XCTAssertEqual(capturedEvent2?.properties?["cryptoSDKVersion"] as? String, "001")
    }
    
    func testShouldNotReportIfNotStarted() {
        // Given a client with user properties set
        let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
    
        // No call to start
        
        client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home))
        
        XCTAssertEqual(posthogMock.screenPropertiesCalled, false)
        
        // It should be the same for any event
        let someEvent = AnalyticsEvent.Error(context: nil,
                                             cryptoModule: .Rust,
                                             cryptoSDK: .Rust,
                                             domain: .E2EE,
                                             eventLocalAgeMillis: nil,
                                             isFederated: nil,
                                             isMatrixDotOrg: nil,
                                             name: .OlmKeysNotSentError,
                                             timeToDecryptMillis: nil,
                                             userTrustsOwnIdentity: nil,
                                             wasVisibleToUser: nil)
        client.capture(someEvent)
        
        XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, false)
        
        // start now
        client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
        XCTAssertEqual(posthogMock.optInCalled, true)
        
        client.capture(someEvent)
        XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, true)
    }
}
