//
//  AWSMobileClientBaseTests.swift
//  AWSMobileClientTests
//

import XCTest

@testable import AWSMobileClient
import AWSAuthCore
import AWSCognitoIdentityProvider
import AWSTestResources

class AWSMobileClientTestBase: XCTestCase {
    static let networkRequestTimeout: TimeInterval = 60.0
    
    static var cognitoIdentity: AWSCognitoIdentity!
    static var userPoolsAdminClient: AWSCognitoIdentityProvider!
    
    static var userPoolId: String!
    static var sharedEmail: String!
    static var identityPoolId: String!
    static var sharedPassword: String!
    
    // Access token with just the "sub" claim
    static var testAccessTokenWithSub: String = """
        eyJraWQiOiJzU01EYmZyQ21pb3FrbEVRZFprNXl6UmszekxSTlo4aGlGMnlxdVFZbVM0PSIsImFsZyI6IlJTMjU2In0.\
        eyJzdWIiOiI3YTQyNTFmMS04MmEyLTQxNzgtOWZhOS1mNmE3MTc1RCJ9.\
        a
        """
    
    // Access token with just the "origin_jti" claim
    static var testAccessTokenWithOriginJTI: String = """
        eyJraWQiOiIwTmxhQUhzbmtwQW5zbHBzUFhHWkJKcVJoR3E5WTkwckwweXpaWUV1OTJZPSIsImFsZyI6IlJTMjU2In0.\
        eyJvcmlnaW5fanRpIjoiMzM2MWFkZDMtMDIwNS00NTY1LTk0MjQtMDQ3YWQ2N2Y0MjhmZWwifQ.\
        a
        """
    
    override class func setUp() {
        AWSDDLog.sharedInstance.logLevel = .verbose
        AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
        sharedEmail = AWSTestConfiguration.getIntegrationTestConfigurationValue(forPackageId: "mobileclient",
                                                                                configKey: "email_address")
        sharedPassword = AWSTestConfiguration.getIntegrationTestConfigurationValue(forPackageId: "mobileclient",
                                                                                configKey: "test_password")

        let commonTestConfiguration = AWSTestConfiguration.getIntegrationTestConfiguration(forPackageId: "common")
        
        let awsConfig = getAWSConfiguration()
        let userPoolConfig = awsConfig["CognitoUserPool"] as! [String: [String: Any]]
        userPoolId = (userPoolConfig["Default"]!["PoolId"] as! String)
        
        let credentialProviderConfig = awsConfig["CredentialsProvider"] as! [String: [String: [String: String]]]
        identityPoolId = credentialProviderConfig["CognitoIdentity"]!["Default"]!["PoolId"]
        
        let testConfigurationJSON = loadTestConfigurationFromFile()
        let credentialsTestConfiguration = testConfigurationJSON["credentials"] as! [String: Any]
        let credentialsProvider = AWSBasicSessionCredentialsProvider(accessKey: credentialsTestConfiguration["accessKey"] as! String,
                                                                     secretKey: credentialsTestConfiguration["secretKey"] as! String,
                                                                     sessionToken: credentialsTestConfiguration["sessionToken"] as! String)
        
        let region = (commonTestConfiguration["region"] as! String).aws_regionTypeValue()
        let configuration = AWSServiceConfiguration(region: region, credentialsProvider: credentialsProvider)!
        
        AWSCognitoIdentityProvider.register(with: configuration, forKey: "TEST")
        AWSCognitoIdentity.register(with: configuration, forKey: "TEST")
        
        cognitoIdentity = AWSCognitoIdentity(forKey: "TEST")
        userPoolsAdminClient = AWSCognitoIdentityProvider(forKey: "TEST")
        initializeMobileClient()
    }
    
    override func tearDown() {
        AWSMobileClient.default().signOut()
        AWSMobileClient.default().clearCredentials()
        AWSMobileClient.default().clearKeychain()
    }
    
    //MARK: Helper methods
    
    static func loadTestConfigurationFromFile() -> [String: Any] {
        return AWSTestConfiguration.getTestConfiguration() as! [String: Any]
    }

    static func getAWSConfiguration() -> [String: Any] {
        let mobileClientConfig = AWSTestConfiguration.getIntegrationTestConfiguration(forPackageId: "mobileclient")
        let awsconfiguration = mobileClientConfig["awsconfiguration"] as! [String: Any]
        return awsconfiguration
    }
    
    static func initializeMobileClient() {
        let testCase = XCTestCase()
        let mobileClientIsInitialized = testCase.expectation(description: "AWSMobileClient is initialized")
        
        AWSMobileClient.default().initialize() { (userState, error) in
            if let error = error {
                XCTFail("Encountered unexpected error in initialize: \(error.localizedDescription)")
                return
            }
            
            guard let userState = userState else {
                XCTFail("userState is unexpectedly empty initializing AWSMobileClient")
                return
            }
            
            if userState != UserState.signedOut {
                AWSMobileClient.default().signOut()
            }
            mobileClientIsInitialized.fulfill()
        }
        testCase.wait(for: [mobileClientIsInitialized], timeout: 5)
    }
    
    func signIn(username: String, password: String? = nil, verifySignState: SignInState = .signedIn) {
        let passwordToUse = password ?? AWSMobileClientTestBase.sharedPassword!
        let signInWasSuccessful = expectation(description: "signIn was successful")
        AWSMobileClient.default().signIn(username: username, password: passwordToUse) { (signInResult, error) in
            if let error = error {
                XCTFail("User login failed: \(error.localizedDescription)")
                return
            }
            
            guard let signInResult = signInResult else {
                XCTFail("User login failed, signInResult unexpectedly nil")
                return
            }
            XCTAssertEqual(signInResult.signInState, verifySignState, "Could not verify sign in state")
            signInWasSuccessful.fulfill()
        }
        wait(for: [signInWasSuccessful], timeout: AWSMobileClientTestBase.networkRequestTimeout)
    }
    
    func confirmSign(challengeResponse: String, userAttributes:[String:String] = [:], verifySignState: SignInState = .signedIn) {
        
        let signInConfirmWasSuccessful = expectation(description: "signIn confirm was successful")
        AWSMobileClient.default().confirmSignIn(challengeResponse: challengeResponse,
                                                userAttributes: userAttributes) {
                                                    (signInResult, error) in
                                                    
                                                    if let error = error {
                                                        XCTFail("Sign in confirm failed: \(error.localizedDescription)")
                                                        return
                                                    }
                                                    if let signInResult = signInResult {
                                                        XCTAssertEqual(signInResult.signInState, verifySignState, "Could not verify sign in state")
                                                    }
                                                    
                                                    signInConfirmWasSuccessful.fulfill()
        }
        wait(for: [signInConfirmWasSuccessful], timeout: AWSMobileClientTestBase.networkRequestTimeout)
    }
    
    func signUpUser(username: String,
                    customUserAttributes: [String: String]? = nil,
                    clientMetaData: [String: String]? = nil,
                    signupState: SignUpConfirmationState = .unconfirmed) {
        var userAttributes = ["email": AWSMobileClientTestBase.sharedEmail!]
        if let customUserAttributes = customUserAttributes {
            userAttributes.merge(customUserAttributes) { current, _ in current }
        }
        
        let signUpExpectation = expectation(description: "successful sign up expectation.")
        AWSMobileClient.default().signUp(
            username: username,
            password: AWSMobileClientTestBase.sharedPassword,
            userAttributes: userAttributes,
            clientMetaData: clientMetaData ?? [:]) { (signUpResult, error) in
                if let error = error {
                    var errorMessage: String
                    if let mobileClientError = error as? AWSMobileClientError {
                        errorMessage = mobileClientError.message
                    } else {
                        errorMessage = error.localizedDescription
                    }
                    XCTFail("Unexpected failure: \(errorMessage)")
                    return
                }
                
                guard let signUpResult = signUpResult else {
                    XCTFail("signUpResult unexpectedly nil")
                    return
                }
                
                switch(signUpResult.signUpConfirmationState) {
                case .confirmed:
                    print("User is signed up and confirmed.")
                case .unconfirmed:
                    if let codeDeliveryDetails = signUpResult.codeDeliveryDetails {
                        print("User is not confirmed and needs verification via \(codeDeliveryDetails.deliveryMedium) sent at \(codeDeliveryDetails.destination!)")
                    } else {
                        print("User is not confirmed but no code delivery details were set")
                    }
                case .unknown:
                    print("Unexpected case")
                }
                
                XCTAssertTrue(signUpResult.signUpConfirmationState == signupState, "User is expected to be marked as \(signupState).")
                XCTAssertNotNil(signUpResult.userSub)
                signUpExpectation.fulfill()
        }
        
        wait(for: [signUpExpectation], timeout: AWSMobileClientTestBase.networkRequestTimeout)
    }
    
    func adminVerifyUser(username: String) {
        guard let adminConfirmSignUpRequest = AWSCognitoIdentityProviderAdminConfirmSignUpRequest() else {
            XCTFail("Unable to create adminConfirmSignUpRequest")
            return
        }
        
        adminConfirmSignUpRequest.username = username
        adminConfirmSignUpRequest.userPoolId = AWSMobileClientTestBase.userPoolId
        
        AWSMobileClientTestBase.userPoolsAdminClient.adminConfirmSignUp(adminConfirmSignUpRequest).continueWith(block: { (task) -> Any? in
            if let error = task.error {
                XCTFail("Could not confirm user. Failing the test: \(error)")
            }
            return nil
        }).waitUntilFinished()
    }
    
    func signUpAndVerifyUser(username: String, customUserAttributes: [String: String]? = nil) {
        signUpUser(username: username, customUserAttributes: customUserAttributes)
        adminVerifyUser(username: username)
    }
    
    func adminCreateUser(username: String, temporaryPassword: String, userAttributes: [String: String] = [:]) {
        guard let adminCreateUserRequest = AWSCognitoIdentityProviderAdminCreateUserRequest() else {
            XCTFail("Unable to create adminCreateUserRequest")
            return
        }
        let userAttributesTransformed = userAttributes.map {AWSCognitoIdentityUserAttributeType.init(name: $0, value: $1) }
        adminCreateUserRequest.username = username
        adminCreateUserRequest.temporaryPassword = temporaryPassword
        adminCreateUserRequest.userAttributes = userAttributesTransformed
        adminCreateUserRequest.userPoolId = AWSMobileClientTestBase.userPoolId
        AWSMobileClientTestBase.userPoolsAdminClient.adminCreateUser(adminCreateUserRequest).continueWith { (task) -> Any? in
            if let error = task.error {
                XCTFail("Could not create user. Failing the test: \(error)")
            }
            return nil
        }.waitUntilFinished()
    }

    /// Removes the access token's expiration key from the keychain, which forces the SDK into the
    /// "refresh token expired" flow
    /// - Parameter username: the username for which to invalidate the token
    func invalidateRefreshToken(username: String) {
        let key = getTokenKeychainKey(for: username)
        getKeychain().removeItem(forKey: key)
    }

    /// Sets access token's expiration date in the keychain to a past date, which forces the SDK into the
    /// "access token expired" flow
    /// - Parameter username: the username for which to invalidate the token
    func invalidateAccessToken(username: String) {
        let key = getTokenKeychainKey(for: username)
        let pastDate = Date(timeIntervalSinceNow: -1)
        let formattedDate = ISO8601DateFormatter().string(from: pastDate)
        let dateData = formattedDate.data(using: .utf8)
        getKeychain().setData(dateData, forKey: key)
    }
    
    func setAccessToken(for username: String,  using accessToken: String) {
        let namespace = "\(AWSMobileClient.default().userPoolClient!.userPoolConfiguration.clientId).\(username)"
        let key = "\(namespace).accessToken"
        let data = accessToken.data(using: .utf8)
        getKeychain().setData(data, forKey: key)
    }

    private func getTokenKeychainKey(for username: String) -> String {
        let namespace = "\(AWSMobileClient.default().userPoolClient!.userPoolConfiguration.clientId).\(username)"
        let key = "\(namespace).tokenExpiration"
        return key
    }

    private func getKeychain() -> AWSUICKeyChainStore {
        let bundleID = Bundle.main.bundleIdentifier
        let keychain = AWSUICKeyChainStore(service: "\(bundleID!).\(AWSCognitoIdentityUserPool.self)")
        return keychain
    }
}

