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

WFOAuth2SessionImp.lua

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

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

NOTICE: All information contained herein is, and remain 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 may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

This object maintains the state for a single OAuth2 user session. A separate
message processor with this object is created for each session to avoid the
problems associated with communicating mutable state across threads.

This object should only be accessed via WFOAuth2Session from a client thread.

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

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

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

	-- WF.core.lua
local KSData = require 'KSData'
local WFRequestDispatcher = require 'WFRequestDispatcher'

	-- WF.core.os
local KSKeychainService = require 'KSKeychainService'
local WFLocalizationUtils = require 'WFLocalizationUtils'

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

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFProgressReporter = require 'WFProgressReporter'

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

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

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

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

local logger = WFLogger 'WFOAuth2SessionImp'
-- :setLevel( 'DEBUG' )

local requestNewTokens -- forward declaration

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

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

-- @mark WFOAuth2SessionImp class

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

local WFOAuth2SessionImp = {

	_sessionId = false,
		-- (string) client ID (typically Oz-style UUID)

	_serverBase = false,
		-- (string) base URL for Oz server being used (e.g. "https://photos-stage.adobe.io")

	_accessAndRefreshTokens = false,
		-- (ReplaySubject< accessToken, refreshToken, optDeviceToken >)

	_validAccessAndRefreshTokens = false,
		-- (Observable< accessToken, refreshToken, optDeviceToken >)

	_refreshRequests = false,
		-- (Subject< table with accessToken, refreshToken, optDeviceToken, authHeader >)

	_refreshNeeded = false,
		-- (Observable< accessToken, refreshToken, optDeviceToken >)

	_refreshSubscribeHandle = false,
		-- (Disposable) for closing down _refreshNeeded subscription

	_requestNewTokensSubscribeHandle = false,
		-- (Disposable) for closing down requestNewTokens flow if in flight

	_hasValidLogin = false,
		-- (ReplayObservable< boolean >)

	_languagePrefString = false,
		-- (string) cached language preference string

	_httpStack = false,
		-- (table) namespace for HTTP requests (can be replaced for debugging)

	_nativeTokenFreshProviderModule = false,
		-- (table) Lua module that provides a WFRxNativeObservable matching function
		-- to do the IMS token refresh

	WFCloseSupport.addCloseSupportMethods,

}

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

function WFOAuth2SessionImp:__init( sessionIdAndMaybeServerBase, nativeTokenRefreshModule )

	check( 'WFOAuth2SessionImp', '+string', 'table|nil' )

	local i, j = string.find( sessionIdAndMaybeServerBase, ':::', 1, true )

	if i then

		self._sessionId = string.sub( sessionIdAndMaybeServerBase, 1, i - 1 )
		self._serverBase = string.sub( sessionIdAndMaybeServerBase, j + 1 )

		if string.len( self._serverBase ) == 0 then
			self._serverBase = nil
		end

	else

		self._sessionId = sessionIdAndMaybeServerBase

	end


	logger:infof( "Created session imp ID %s via %s", self._sessionId )

	self._httpStack = require 'WFRxHttpClient'

	self._nativeTokenFreshProviderModule = nativeTokenRefreshModule

	self._accessAndRefreshTokens = ReplaySubject( 1 )

	-- This Observable should have an available value anytime
	-- there is a valid access + refresh token pair and should stall
	-- when there's not.

	self._validAccessAndRefreshTokens = self._accessAndRefreshTokens
		:filter( function( accessToken )
			local isValid = accessToken ~= 'error' and accessToken ~= 'refresh'
			logger:debugf( "_accessAndRefreshTokens updated, isValid = %s", isValid )
			return isValid
		end )

	-- This ReplayObservable derives from the accessTokens storage
	-- and can be used by the outside world to determine whether
	-- there is a valid session.

	self._hasValidLogin = self._accessAndRefreshTokens
		:filter( function( accessToken )
			return accessToken ~= 'refresh'
		end )
		:map( function( accessToken )
			return accessToken ~= 'error'
		end )
		:startWith( true )
		:tap( function( hasValidLogin )
			logger:debugf( "_hasValidLogin = %s", hasValidLogin )
		end )
		:distinctUntilChanged()

	-- This Subject should be triggered whenever we receive a 401
	-- (invalid token) response, regardless of whether the token is
	-- valid or not.

	self._refreshRequests = Subject()

	-- This Observable should fire whenever we need to ask the OAuth2
	-- identity server for a new access token.

	self._refreshNeeded = self._refreshRequests

		-- Merge refresh request with current known-valid token stream.

		:tap( function()
			logger:debugf( "_refreshRequests received an update" )
		end )

		:withLatestFrom( function( refreshRequest, accessToken, refreshToken, optDeviceToken )
			return refreshRequest, accessToken, refreshToken, optDeviceToken
		end, self._validAccessAndRefreshTokens )

		:filter( function( refreshRequest, accessToken, refreshToken, optDeviceToken )
			return type( refreshRequest ) == 'table' and refreshRequest.accessToken == accessToken
		end )

	self._refreshSubscribeHandle = self._refreshNeeded
		:subscribe( function( refreshRequest, accessToken, refreshToken, optDeviceToken )
			return requestNewTokens( self, refreshRequest, accessToken, refreshToken, optDeviceToken )
		end )

	-- Read initial keys. It's possible that there will be a race condition
	-- in which somebody provides the keys shortly *after* this OAuth2 session
	-- is created. To mitigate this risk, we make two attempts to read the
	-- keys a few seconds apart.

	local readKeychainHandle = WFRxRetryObservable {

		maxAttempts = 4,
		retryDelay = 1,

		factory = function()

			return KSKeychainService.read( self._sessionId )

				:map( function( rawJson )

					local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull( rawJson )

					if type( decodedJson ) == 'table' then
						logger:infof( "Found existing access and refresh token in keychain" )
						return decodedJson.access_token, decodedJson.refresh_token
					end

				end )

				:selectMany( function( accessToken, refreshToken )

					return KSKeychainService.read( self._sessionId .. '.device_token' )
						:map( function( deviceToken )

							return accessToken, refreshToken, deviceToken

						end )

				end )

				:flatMap( function( accessToken, refreshToken, deviceToken )

					if not ( accessToken ) then
						return Observable.throw( "No tokens in keychain. Do not panic: This may be normal." )
					end

					return Observable.just( accessToken, refreshToken, deviceToken )

				end )

				:tap(
					function( ... )
						logger:debugf( "Got data from keychain" )
					end,
					function( ... )
						logger:errorf( "Failed to get data from keychain: %s, %s, %s", ... )
					end,
					function()
						logger:debugf( "Keychain read completed" )
					end )

		end,

	}
	:subscribe(

		function( accessToken, refreshToken, optDeviceToken )

			logger:infof( "Updating _accessAndRefreshTokens from stored" )

			self._accessAndRefreshTokens:onNext( accessToken, refreshToken, optDeviceToken )

		end,

		function( message )

			logger:errorf( "Forcing logout because %s", message )

			self._accessAndRefreshTokens:onNext( 'error', "no tokens stored" )

		end )

	-- Cancel the above operation if we get tokens from somewhere else.

	self._accessAndRefreshTokens

		:take( 1 )

		:tap( function()

			logger:infof( "Reading tokens from keychain canceled because we got tokens via %s",
						  ':traceback:' )

			readKeychainHandle:dispose()

		end )

		:call( runUntilDone )

end

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

function WFOAuth2SessionImp:close()

	if self._refreshRequests then
		self._refreshRequests:onCompleted()
		self._refreshRequests:dispose()
		self._refreshRequests = nil
	end

	if self._refreshSubscribeHandle then
		self._refreshSubscribeHandle:dispose()
		self._refreshSubscribeHandle = nil
	end

	if self._requestNewTokensSubscribeHandle then
		self._requestNewTokensSubscribeHandle:dispose()
		self._requestNewTokensSubscribeHandle = nil
	end

end

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

local kUnchanged = {}

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

-- returns an observable that only signals completion
local function updateKeychain( key, value )

	if value == nil then
		return KSKeychainService.delete( key )
	elseif value == kUnchanged then
		return Observable.empty()
	else
		return KSKeychainService.write( key, value )
	end

end

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

function WFOAuth2SessionImp:storeTokens( accessToken, optRefreshToken, optDeviceToken )

	check( 'WFOAuth2SessionImp', '+string', '?string', '?string' )

	logger:infof( "Store tokens for session ID %s", self._sessionId )

	self._accessAndRefreshTokens:onNext( accessToken, optRefreshToken, optDeviceToken )

	local userInfo = KSJsonEncoder.encodeOzUploadJson {
		access_token = accessToken,
		refresh_token = optRefreshToken,
	}

	return updateKeychain( self._sessionId, userInfo )

		:merge( updateKeychain( self._sessionId .. '.device_token', optDeviceToken ) )

end

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

function WFOAuth2SessionImp:fetchTokens()

	check( 'WFOAuth2SessionImp' )

	return KSKeychainService.read( self._sessionId )

		:map( function( rawJson )

			local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull( rawJson )

			if type( decodedJson ) == 'table' then
				logger:infof( "Found existing access and refresh token in keychain" )
				return decodedJson.access_token, decodedJson.refresh_token
			end

			return nil

		end )

		:flatMap( function( accessToken, refreshToken )

			return KSKeychainService.read( self._sessionId .. '.device_token' )
				:map( function( deviceToken )

					return accessToken, refreshToken, deviceToken

				end )

		end )

end

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

local function statusCodeFromMaybeHeaders( maybeHeaders )

	if type( maybeHeaders ) == 'table' then

		local statusCode = maybeHeaders.status_code

		if type( statusCode ) == 'string' then
			return statusCode
		end

	end

end

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

local function maybeRequestNewTokensFromExternalProvider( self, refreshRequest, accessToken, refreshToken, optDeviceToken )

	if self._nativeTokenFreshProviderModule then

		return WFRxNativeObservable( self._nativeTokenFreshProviderModule.requestNewTokens, accessToken, refreshToken, optDeviceToken )

			:flatMap( function( tokens )

				assert( type( tokens ) == 'table',
					"Native provider needs to provide a table consisting the refreshed tokens" )

				local accessToken = tokens.accessToken
				local refreshToken = tokens.refreshToken
				local optDeviceToken = tokens.deviceToken

				local WFPlatformInfo = require 'WFPlatformInfo'
                		-- This check is being made for the reason that deviceToken can be empty for few locales like Chinese
				if WFPlatformInfo.isAndroid and optDeviceToken and optDeviceToken == '' then
					optDeviceToken = nil
				end
				return self:storeTokens( accessToken, refreshToken, optDeviceToken )
					:concat( Observable.just( accessToken, refreshToken, optDeviceToken ) )

			end )

			-- Log results.

			:tap(
				function( accessToken, refreshToken, optDeviceToken )
					logger:infof( "Successfully refreshed tokens via External Provider" )
				end,
				function( err )
					logger:errorf( "Failed to refresh tokens %s via External Provider", err )
				end )

	end

	return nil

end

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

requestNewTokens = function( self, refreshRequest, accessToken, refreshToken, optDeviceToken )

	-- Post a magic value to the token stream. This will cause all future requests
	-- to stall until we can post a new access token. This will also cause any
	-- further refresh token requests that originated from the now-expired access
	-- token to get discarded since we've already got a refresh request in flight.

	logger:debugf( "requestNewTokens starting" )

	if self._requestNewTokensSubscribeHandle then
		logger:warn "requestNewTokens: a previous subscribe handle still exists"
	end

	self._accessAndRefreshTokens:onNext( 'refresh', refreshToken, optDeviceToken )

	local requestNewTokensFlow = maybeRequestNewTokensFromExternalProvider( self, refreshRequest,
									accessToken, refreshToken, optDeviceToken )

	if not requestNewTokensFlow then
		requestNewTokensFlow = Observable.throw( "requestNewTokens method should be provided by clients." )
	end

	requestNewTokensFlow
		:take( 1 )

	self._requestNewTokensSubscribeHandle = requestNewTokensFlow:subscribe(

		function( accessToken, refreshToken, optDeviceToken )

			-- Congratulations, you have successfully navigated the
			-- OAuth2 minefield. Save these tokens and hope that you
			-- are as fortunate next time around.

			-- Posting to the access token stream will unblock any
			-- requests that were stalled while we waited for new tokens.

			logger:debugf( "requestNewTokens finished successfully" )

			self._requestNewTokensSubscribeHandle = nil

			self._accessAndRefreshTokens:onNext( accessToken, refreshToken, optDeviceToken )

		end,

		function( data, headers )

			-- No dice. Giving up and signaling failure for all
			-- future calls.

			logger:errorf( "requestNewTokens failed\n\ndata = %s\n\nheaders = %s\n\n",
						   data, headers )

			self._requestNewTokensSubscribeHandle = nil

			self._accessAndRefreshTokens:onNext( 'error', tostring( data ) )

			local statusCode = statusCodeFromMaybeHeaders( headers )

			if statusCode and statusCode >= '400' and statusCode <= '499' then

				KSKeychainService.delete( self._sessionId )
				KSKeychainService.delete( self._sessionId .. '.device_token' )

			end

		end )

end

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

local function maybeDeferredCloseData( maybeData )

	if KSData.isData( maybeData ) then
		WFRequestDispatcher.deferredClose( maybeData )
	end

end

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

local function acceptLanguagePreferencesString( self )

	if self._languagePrefString then
		return self._languagePrefString
	end

	local localeId = WFLocalizationUtils.getLocaleId() -- e.g. "en-US"
	local langCode = WFLocalizationUtils.getLanguageCode() -- e.g. "en"

	-- This fallback shouldn't be necessary, but better this
	-- than sending some badly-formatted string to Oz.

	-- Purposely don't cache this bad answer, maybe it will work next time.

	if not localeId then
		logger:error( "No locale ID available" )
		return langCode or ''
	end

	-- e.g. "Accept-Language: en-US, en;q=0.8"
	-- Indicates that the region-specific language identifier is highest preference,
	-- (default q=1), but fallback to any other regional variant of the same language.
	-- q=0.8 is a totally arbitrary value less than 1. Doesn't really matter the value we
	-- choose if there is not a third or fourth preference.

	self._languagePrefString = string.format( '%s, %s;q=0.8', localeId, langCode )

	return self._languagePrefString

end

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

local function getPut_imp( self, url, args, getPutInnerFunc )

	logger:infof( "getPut_imp for URL %s", url )

	local function debugOutput( data, headers )

		local statusCode = statusCodeFromMaybeHeaders( headers )

		if statusCode and statusCode < '400' then
			logger:debugf( "normal response on URL %s\n\nheaders = %s", url, headers )
		elseif data ~= 'cancelled' and data ~= 'requestInProgress' then
			logger:warnf( "error on URL %s\n\ndata = %s\n\nheaders = %s", url, data, headers )
		end

	end

	local _accessToken, _refreshToken, _maybeDeviceToken

	return WFRxRetryObservable {

		maxAttempts = 2,
		retryDelay = .5, -- give time for token refresh to at least start

		factory = function( count )

			logger:debugf( "getPut_imp factory function starting for URL %s", url )

			return self._validAccessAndRefreshTokens

				:timeout( 10,
						  count > 1 and "Unable to refresh tokens" or "No access token available",
						  WFRxScheduler.sharedScheduler() )

				:take( 1 )

				:flatMapLatest( function( accessToken, refreshToken, maybeDeviceToken )

					logger:debugf( "getPut_imp factory function got tokens, ready to start URL %s", url )

					assert( accessToken, "missing access token" )

					-- Remember the current token set in case we have to trigger
					-- a refresh request.

					_accessToken = accessToken
					_refreshToken = refreshToken
					_maybeDeviceToken = maybeDeviceToken

					-- TODO [scouten 2015-07-19]: Restore progress reporting.

					-- Given that we might need to retry, make sure we
					-- don't dispose the data until the show's completely over.

					args = args or {}
					args.retainDataAfterRequest = true

					-- Strip any previous Authorization and x-token-ttl headers.

					local headers = args.requestHeaders or {}
					args.requestHeaders = headers

					local hasAcceptLanguageHeader = false

					for i = #headers, 1, -1 do

						if headers[ i ].header == 'Accept-Language' then
							hasAcceptLanguageHeader = true
						end

						if headers[ i ].header == 'Authorization'
							or headers[ i ].header == 'x-token-ttl'
						then

							if not args.withoutAdobeAuthorization then
								table.remove( headers, i )
							end

						end

					end

					-- Create the request based on current access token.
					if not args.withoutAdobeAuthorization then
						headers[ #headers + 1 ] =
							{ header = 'Authorization', value = 'Bearer ' .. accessToken }
					end

					-- Add an Accept-Language header.

					if not hasAcceptLanguageHeader then
						headers[ #headers + 1 ] =
							{ header = 'Accept-Language', value = acceptLanguagePreferencesString( self ) }
					end

					-- Call the inner get/put function.

					logger:debugf( "sending request to getPutInnerFunc with args = %s", args )

					return getPutInnerFunc( args )
						:tap( debugOutput, debugOutput )

				end )

				-- Make sure we don't leak any data from this response.

				:tap( maybeDeferredCloseData, maybeDeferredCloseData )

				-- In the possibility caller chose to map HTTP error codes to success,
				-- convert OAuth2-related status codes back to errors. We'll catch those
				-- in the retry logic.

				:flatMapLatest( function( data, headers, ... )

					local statusCode = statusCodeFromMaybeHeaders( headers )

					if statusCode == '401' then
						return Observable.throw( data, headers, ... )
					end

					-- TODO [scouten 2015-07-19]: Consider restoring 403 error handling
					-- here? 403 + Oz error 4999 means client kill switch has been
					-- invoked by Oz. I'd prefer that move up to sync agent code.

					return Observable.just( data, headers, ... )

				end )

		end,

		retryFilter = function( data, headers )

			logger:debugf( "getPut_imp retryFilter examining error response to url %s \n\ndata %s headers %s",
						   url, data, headers )

			-- We only retry 401 errors at this point.

			-- 401 (not authorized) mean swe have an invalid access token.
			-- This means we should suspend any further requests and use the
			-- refresh token to try again.

			local statusCode = statusCodeFromMaybeHeaders( headers )

			if statusCode == '401' then

				logger:infof( "Detected invalid access token %s", headers )

				local authHeader = headers[ 'Www-Authenticate' ] or headers[ 'WWW-Authenticate' ]

				-- Note that there is a possible race condition in that several
				-- HTTP requests might have originated with the same now-invalid
				-- access token. By design, _refreshRequests is the fire hose.
				-- We de-duplicate the requests in the construction of
				-- _refreshNeeded (see this class's __init method). Here, we pass
				-- the access token that was used to initiate this HTTP request.
				-- If it's still valid, then we initiate the refresh; if not,
				-- we ignore this refresh request because another request has already
				-- failed and triggered the appropriate refresh.

				self._refreshRequests:onNext {
					accessToken = _accessToken,
					refreshToken = _refreshToken,
					deviceToken = _maybeDeviceToken,
					authHeader = authHeader
				}

				return true

			end

			-- All other errors are not retried.

			return false

		end,

	}

end

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

function WFOAuth2SessionImp:get( url, optArgs, optSequencing )

	check( 'WFOAuth2SessionImp', '+string', '?table', '?string' )

	-- TODO [scouten 2015-07-19]: Restore sequencing.

	local progressReporter
	local progressStatusStream

	if optArgs and optArgs.includeProgress then

		progressReporter = WFProgressReporter( string.format( "get::%s", url ) )
		progressStatusStream = progressReporter:status()

	end

	return getPut_imp( self, url, optArgs,

		function( newArgs )

			if progressStatusStream then

				local packedGetRequest = self._httpStack.get( url, newArgs, optSequencing, progressReporter )

					:map( function( responseData, headers )
						return { responseData, headers }
					end )

					:finally( function()
						progressReporter:done()
					end )

				return Observable.combineLatest( packedGetRequest, progressStatusStream )

					:map( function( responseDataAndHeaders, ... )

						local responseData, headers = unpack( responseDataAndHeaders )

						return responseData, headers, ...

					end )

			else

				return self._httpStack.get( url, newArgs )

			end

		end )


end

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

function WFOAuth2SessionImp:put( url, data, optArgs, optSequencing )

	check( 'WFOAuth2SessionImp', '+string', 'KSData|string|number|table', '?table' )

	local progressReporter
	local progressStatusStream

	if optArgs and optArgs.includeProgress then

		progressReporter = WFProgressReporter( string.format( "put::%s", url ) )
		progressStatusStream = progressReporter:status()

	end

	return getPut_imp( self, url, optArgs,

		function( newArgs )

			if progressStatusStream then

				local packedPutRequest = self._httpStack.put( url, data, newArgs, progressReporter )

					:map( function( responseData, headers )
						return { responseData, headers }
					end )

					:finally( function()
						progressReporter:done()
					end )

				return Observable.combineLatest( packedPutRequest, progressStatusStream )

					:map( function( responseDataAndHeaders, ... )

						local responseData, headers = unpack( responseDataAndHeaders )

						return responseData, headers, ...

					end )

			else

				return self._httpStack.put( url, data, newArgs )

			end

		end )

		:finally( function()
			if KSData.isData( data ) then
				data:close()
			end
		end )

end

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

function WFOAuth2SessionImp:hasValidLogin()

	check( 'WFOAuth2SessionImp' )

	return self._hasValidLogin

end

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

local function deleteAuthKeys( self )

	KSKeychainService.delete( self._sessionId )
	KSKeychainService.delete( self._sessionId .. '.device_token' )

	self._accessAndRefreshTokens:onNext( 'error', "logged out" )

end

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

function WFOAuth2SessionImp:simulateForcedLogout()

	check( 'WFOAuth2SessionImp' )

	logger:error( "Simulate forced logout" )

	deleteAuthKeys( self )

	return Observable.empty()

end

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

local function logout( self )

	return self._accessAndRefreshTokens

		:take( 1 )

		:flatMapLatest( function( accessToken )

			deleteAuthKeys( self )

			if not accessToken or accessToken == 'refresh' then
				logger:debugf( "User is not valid." )
				return Observable.empty()
			end

			return Observable.just( true )

		end )

end

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

function WFOAuth2SessionImp:logout()

	check( 'WFOAuth2SessionImp' )

	return self._hasValidLogin
		:take( 1 )
		:map( function( isValid )

			if isValid then

				return logout( self )

			else

				return Observable.empty()

			end

		end )
		:switch()

end

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

-- @mark miscellaneous

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

function WFOAuth2SessionImp:__tostring()

	check( 'WFOAuth2SessionImp' )

	return string.format( "WFOAuth2SessionImp( %s )", self._sessionId )

end

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

function WFOAuth2SessionImp:DEBUG_replaceHttpStack( httpStack )

	check( 'WFOAuth2SessionImp', 'table' )

	self._httpStack = httpStack

end

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

function WFOAuth2SessionImp:DEBUG_dumpCoverage( path )

	check( 'WFOAuth2SessionImp', '+string' )

	require 'SetupCoverage'.dumpCoverage( path )

	return Observable.empty()

end

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

return Rx.Class 'WFOAuth2SessionImp'( WFOAuth2SessionImp )
