--[[----------------------------------------------------------------------------

WFOzUserTestSession.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.logging
local WFLogger = require 'WFLogger'

	-- WF.core.os
local KSKeychainService = require 'KSKeychainService'
local WFDeviceInfo = require 'WFDeviceInfo'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.core.rxlua
local WFRxRetryObservable = require 'WFRxRetryObservable'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFConfiguration = require 'WFConfiguration'

	-- WF.oz_client.ims
local WFImsAppIdentity = require 'WFImsAppIdentity'

	-- WF.oz_client.sync_engine
local WFOzUserLogin = require 'WFOzUserLogin'

	-- WF.web.http_client
local WFRxHttpClient = require 'WFRxHttpClient'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'
local KSJsonEncoder = require 'KSJsonEncoder'

	-- WF.web.oauth2
local WFOAuth2Session = require 'WFOAuth2Session'

	-- WF.web.utils
local WFURI = require 'WFURI'


--------------------------------------------------------------------------------
--[[

	If you want to use a specific account, you can describe that in your
	config.lua file with a section similar to this:

		WFTestRunner.BypassNoir = {
			oz_email = "wfozusertest+00@gmail.com",
			oz_password = "adobetest",
			oz_other_data = {
				oz_first_name = "Test",
				oz_last_name = "User",
				oz_catalog_name = "TestCatalog",
			},
		}

	If such a section exists, this information will be used and no calls will
	be made to obtain user accounts.

]]--

local overrideAccountInfo = WFConfiguration.WFTestRunner.BypassNoir

local ozAccountServer = "http://sj1010120180138.corp.adobe.com:3848"

--------------------------------------------------------------------------------

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

local logger = WFLogger 'WFOzUserTestSession'
--	:setLevel( 'debug' )

--------------------------------------------------------------------------------

local function getUserInfoFromOzAccountServer( optTestName )

	-- Are we overriding user info?

	if overrideAccountInfo then

		debug.print( "Bypassing user account checkout mechanism. " ..
					 "Using account information from config file instead. " ..
					 "Account ID is: " .. overrideAccountInfo.oz_email )

		return Observable.just( overrideAccountInfo.oz_email,
								overrideAccountInfo.oz_password,
								overrideAccountInfo.oz_other_data )

	end

	-- Nope. Let's go ask for a user.

	local fetchUrl = string.format( "%s/%s?test=%s&machine=%s",
									ozAccountServer, "fetch_account",
									optTestName or "unknown",
									WFURI.escape( WFDeviceInfo.deviceName() or "unknown", 'http_query_key' ) )

	local options = {
		requestMethod = 'GET',
		requestHeaders = {
			{ header = "If-Modified-Since",
			  value = "Sat, 01 Jan 2000 00:00:01 GMT"
			},
		},
		timeout = 10,
	}

	return WFRxRetryObservable {

		maxAttempts = 3,
		retryDelay = 5,

		factory = function()

			return WFRxHttpClient.get( fetchUrl, options )

				-- Don't know if this is still happening, but in May 2014, we were having
				-- trouble with Noir occasionally returning HTTP status 200, but no data.
				-- We convert those responses to errors and retry.

				:map( function( data, headers )

					local statusCode = type( headers ) == 'table' and headers.status_code

					assert( statusCode == '200' and data,
							"Mysterious empty data happened" )

					-- Response looks valid. Decode the JSON.

					local account = KSJsonDecoder.decodeWebSafeJson_stripNull( data )

					return account.oz_email, account.oz_password, account.oz_other_data

				end )

		end,

	}

	:tapOnError( function( data, headers )

		local statusCode = type( headers ) == 'table' and headers.status_code

		if statusCode == '404' then

			debug.print "No more user accounts available to execute tests."

		elseif statusCode == '403' then

			debug.print "This machine has locked the maximum number of Oz accounts. Try again after 5 minutes."

		else

			debug.print( string.format( "Failed to fetch an account for unknown reasons.\n" ..
										"Status Code: %s\n" ..
										"Data: %s\n", statusCode, data ) )

		end

	end )

end


--============================================================================--

-- @mark WFOzUserTestSession class

local WFOzUserTestSession = {

	_rxTest = false,
		-- (WFRxTestCaseRunner) test that we're running with

	_testName = false,
		-- (string) name of test we're running, if available

	_userInfo = false,
		-- (ReplayObservable< email, password, otherOzInfo >)

	_accessToken = false,
		-- (string) saved copy of OAuth2 access token

	_refreshToken = false,
		-- (string) saved copy of OAuth2 refresh token

	_ozServerBaseUrl = false,
		-- (string) base Oz server URL

	_adminServerBaseUrl = false,
		-- (string) base URL for Oz admin server

	_suppressStoreTokens = false,
		-- (boolean) if true, don't store tokens for this client

	_ozUserId = false,
		-- (string) Oz user UUID (if created)

	_apiVersion = false,
		-- (string) Oz API version ('v1.0' or 'v2')

}

--------------------------------------------------------------------------------

function WFOzUserTestSession:__init( rxTestOrTestName, options )

	check( 'WFOzUserTestSession', 'WFRxTestCaseRunner|string', '?table' )

	logger:infof( "WFOzUserTestSession:__init( %s, %s )", rxTestOrTestName, options )

	WFRxHttpClient.setUserAgentAppName( "wf_mac_self_tests_" .. tostring( rxTestOrTestName ) );

	options = options or {}

	if type( rxTestOrTestName ) == 'string' then

		self._testName = rxTestOrTestName

	else

		self._rxTest = rxTestOrTestName

		self._testName = rxTestOrTestName:testName()

	end

	self._testName = options.testName or self._testName
	
	self._suppressStoreTokens = options.suppressStoreTokens

	-- Figure out which server stack we're working with.
	-- Default (for testing) is v2 stage.
	local apiServerBaseUrl = 'https://photos-stage.adobe.io'
	local adminServerBaseUrl = 'https://admin.adbephotos-stage.com'
	self._apiVersion = 'v2'
		
	if options.v1 then
		apiServerBaseUrl = 'https://api.ozstage.com'
		adminServerBaseUrl = apiServerBaseUrl:replace( 'api', 'admin' )

		self._apiVersion = 'v1.0'
	end

	self._ozServerBaseUrl = apiServerBaseUrl
	self._adminServerBaseUrl = adminServerBaseUrl

end

--------------------------------------------------------------------------------
--[[!
	\brief Reserves a user ID from the Oz Account Server and returns the description of that user

	\return Observable< email, password, ozOtherData >
]]--

function WFOzUserTestSession:userInfo()

	if not self._userInfo then

		self._userInfo = getUserInfoFromOzAccountServer( self._testName )
			:replay( nil, 1 )

		self._userInfo:connect()

		if self._rxTest then

			self._rxTest:finally( function()

				return self:releaseUser()

			end )

		end

	end

	return self._userInfo

end

--------------------------------------------------------------------------------
--[[!
	\brief Releases the user (if any) that was previously acquired by this object.

	Should be used only by \c WFAsyncTestCase-based tests. This is handled
	automatically for \c WFRxTestCase-based tests.

	\return Observable<> completes when user is released
]]--

function WFOzUserTestSession:releaseUser()

	logger:infof( "releaseUser via ", ':traceback:' )

	local userInfo = self._userInfo

	if not userInfo then
		logger:debugf( "releaseUser - skipping, no userInfo" )
		return Observable.empty()
	end

	-- Only need to do this once per user.

	self._userInfo = nil

	return userInfo

		:take( 1 )

		:tap( function()
			if self._ozUserId then
				KSKeychainService.delete( self._ozUserId )
			end

			if WFPlatformInfo.isWindows then
				local KSPrefsImpLua = require 'KSPrefsImpLua'
				KSPrefsImpLua.deletePrefsFile()
			end

		end )

		:map( function( userEmail )

			local putUrl = string.format(
				"%s/oz_email/%s/release_account", ozAccountServer, userEmail )

			logger:debugf( "releaseUser - will send release request to %s", putUrl )

			local options = {
				requestHeaders = {
					{ header = 'Content-Type', value = 'application/json' },
					{ header = 'Accept', value = 'application/json' },
				},
				requestMethod = 'PUT',
				maxAttempts = 3,
				retryDelay = 2,
				timeout = 10,
			}

			local jsonData = KSJsonEncoder.encodeOzUploadJson { empty = true }

				-- GRUNGE [scouten 2014-06-24]: Is this delay still necessary, now that
				-- we've cleaned up the premature start problem?
				-- [jandrew 2014-07-24]: Yes, it is.

			return Observable.timer( 3, WFRxScheduler.sharedScheduler() )

				:flatMapLatest( function( count )

					logger:infof( "releaseUser - release attempt %s", count )

					return WFRxHttpClient.put( putUrl, jsonData, options )

				end )
				:tap(
					function( data, headers )

						logger:infof( "Checking in account succeeded with\n\nresponse %s\n\nheaders %s\n\n",
									  tostring( data ), headers )

					end,
					function( data, headers )

						logger:errorf( "Checking in account FAILED with\n\nresponse %s\n\nheaders %s\n\n",
									   tostring( data ), headers )

					end )
					
				:catch( function()
					return Observable.just()
				end )
				
				:where( function()

					return false

				end )

		end )

		:switch()

		:call( runUntilDone )

end

--------------------------------------------------------------------------------

-- @mark Oz user ID acquisition

--------------------------------------------------------------------------------

local function getOzAccountInfoFromToken( self, baseServerUrl, accessToken )

    local imsClientId = WFImsAppIdentity.getClientId()

	local url = WFURI.resolve( baseServerUrl,
							   string.format( '/%s/accounts/00000000000000000000000000000000',
							   				  self._apiVersion ) )

	local options = {
		requestHeaders = {
			{ header = "If-Modified-Since", value = "Sat, 01 Jan 2000 00:00:01 GMT" },
			{ header = 'Authorization', value = 'Bearer ' .. accessToken },
            { header = 'X-Api-Key', value = imsClientId },
		},
		timeout = 10,
	}

	logger:debugf( "getOzAccountInfoFromToken - url = %s, token = %s", url, accessToken )

	return WFRxRetryObservable {

		maxAttempts = 3,
		retryDelay = 5,

		factory = function()

			logger:infof( "getOzAccountInfoFromToken - GET %s", url )

			local result = WFRxHttpClient.get( url, options )
				:map( KSJsonDecoder.decodeWebSafeJson_stripNull )

			if logger:will_info() then

				result = result:tap(

					function( x )
						logger:info( "onNext %s", x )
					end,
					function( x )
						logger:info( "onError %s", x )
					end,
					function()
						logger:info( "onCompleted" )
					end )

			end

			return result

		end,

	}

end

--------------------------------------------------------------------------------

local function deleteAccount( self, ozUserId, accessToken )

	-- curl -X DELETE -H "Authorization: Bearer $access_token" -v
		-- $PRTCL://$HOST/v1.0/accounts/$account_id

    local imsClientId = WFImsAppIdentity.getClientId()

	local url = WFURI.resolve( self._adminServerBaseUrl,
							   string.format( '/%s/accounts/%s',
							   				  self._apiVersion,
											  ozUserId ) )

	local options = {
		requestHeaders = {
			{ header = 'Authorization', value = 'Bearer ' .. accessToken },
            { header = 'X-Api-Key', value = imsClientId },
		},
		requestMethod = 'DELETE',
		timeout = 10,
	}

	return WFRxHttpClient.get( url, options )

end

--------------------------------------------------------------------------------

local function getCleanOzUserId( self, baseServerUrl, accessToken, refreshToken )

	self._accessToken = accessToken
	self._refreshToken = refreshToken

	return WFRxRetryObservable {

		maxAttempts = 10,
		retryDelay = 3,

		factory = function( count )

			-- Get or create an Oz user account from access token.

			logger:debugf( "getCleanOzUserId - attempt #%s", count )

			return getOzAccountInfoFromToken( self, baseServerUrl, accessToken )

				:map( function( ozAccountInfo )

					logger:debugf( "getCleanOzUserId - accountInfo = %s", ozAccountInfo )
					return ozAccountInfo.id and ozAccountInfo.id:sub( -32 )

				end )

				-- Now delete that account so we are sure we have a
				-- fully clean account.

				:selectMany( function( userId )

					logger:debugf( "getCleanOzUserId - deleting account %s", userId )
					return deleteAccount( self, userId, accessToken )

				end )

				-- Reload account from Renga to Oz.

				:selectMany( function()

					logger:debug( "getCleanOzUserId - reloading account from IMS ..." )
					return getOzAccountInfoFromToken( self, baseServerUrl, accessToken )

				end )

				-- Verify that we actually got a valid account.

				:map( function( ozAccountInfo )

					logger:debugf( "getCleanOzUserId - new accountInfo = %s", ozAccountInfo )

					local userId = ozAccountInfo.id and ozAccountInfo.id:sub( -32 )

					local wcdGuid = ozAccountInfo.wcd_guid
					local accountStatus
					if self._apiVersion == 'v1.0' then  
						accountStatus = ozAccountInfo.status
					else
						accountStatus = ozAccountInfo.entitlement.status
					end 
					if accountStatus ~= 'created' then
						error( string.format( "Failed to create an account for %s (%s)", 
								ozAccountInfo.email, ozAccountInfo.wcd_guid ) )
					end

					return userId, wcdGuid, accountStatus

				end )

		end,

	}

	:flatMap( function( userId, wcdGuid, accountStatus )

		if self._suppressStoreTokens then 
			return Rx.Observable.just( userId, wcdGuid, accountStatus ) 
		end

		-- Store new tokens from account.

		local tempOAuth2Session = WFOAuth2Session( userId )

		self._ozUserId = userId

		return tempOAuth2Session:storeTokens( accessToken, refreshToken )
			
			:merge( Rx.Observable.just( userId, wcdGuid, accountStatus ) )
			
			:tap( function()
				tempOAuth2Session:close()
			end )
			
			:call( runUntilDone )

	end )
	

end

--------------------------------------------------------------------------------
--[[!
	\brief Acquire a new, uninitialized user account on Oz.

	\return Observable< user ID >
]]--

function WFOzUserTestSession:getCleanOzAccount()

	local accessToken

	logger:debug( "getCleanOzAccount" )

	-- Get username / password pair.

	return self:userInfo()

		-- Convert that to tokens from IMS.

		:selectMany( function( email, password, otherOzData )

			logger:infof( "getCleanOzAccount - got userInfo email = %s", email )

			return WFOzUserLogin.tokensFromEmailAndPassword( self._ozServerBaseUrl,
															 self._apiVersion,
															 email, password )
				:tap( function( _, _accessToken )

					logger:debug( "getCleanOzAccount - got access token from IMS" )

					accessToken = _accessToken

				end )

		end )

		-- Now attempt to delete any existing account on Oz.

		:selectMany( function( ... )

			-- TEMPORARY [scouten 2015-07-19]: Remove the wrapper and the
			-- prepending of self when we fully cut over to new WF OAuth2 code.

			return getCleanOzUserId( self, ... )

		end )

		-- If IMS says to refresh, then go through the Oz account dance again.

		:selectMany( function( ozUserId )

			logger:debugf( "getCleanOzAccount - IMS response was = %s", ozUserId )

			if ozUserId == 'refresh' then
				return getCleanOzUserId( self, self._ozServerBaseUrl, accessToken )
			else
				return Observable.just( ozUserId )
			end

		end )

		-- Finally extract the user ID by itself

		:map( function( ozUserId, wcdGuid, accountStatus )

			logger:debugf( "getCleanOzAccount - finished user ID = %s", ozUserId )

			return ozUserId -- strip other members of tuple

		end )

end

--------------------------------------------------------------------------------

function WFOzUserTestSession:accessAndRefreshTokens()

	check( 'WFOzUserTestSession' )

	return self._accessToken, self._refreshToken

end

--------------------------------------------------------------------------------

function WFOzUserTestSession:reloadAccessAndRefreshTokens()

	check( 'WFOzUserTestSession' )

	-- Store new tokens from account.

	local tempOAuth2Session = WFOAuth2Session( self._ozUserId )

	return tempOAuth2Session:storeTokens( self._accessToken, self._refreshToken )
		
		:tap( function()
			tempOAuth2Session:close()
		end )
		
		:call( runUntilDone )

end

--------------------------------------------------------------------------------

function WFOzUserTestSession:ozServerBaseUrl()

	check( 'WFOzUserTestSession' )

	return self._ozServerBaseUrl

end

--------------------------------------------------------------------------------

return Rx.Class 'WFOzUserTestSession'( WFOzUserTestSession )
