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

WFCatOzTestSession.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2019 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
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.

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

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

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local KSTableUtils = require 'KSTableUtils'

	-- WF.core.table
local WFTableArchiving = require 'WFTableArchiving'

	-- WF.managed_catalog
local WFCatCollaboration = require 'WFCatCollaboration'
local WFCatTestSession = require 'WFCatTestSession'

	-- WF.oz_client.managed_catalog
local WFCatBinaryUploader = require 'WFCatBinaryUploader'
local WFCatConflictResolver = require 'WFCatConflictResolver'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'

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

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

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

local check = assert( WFArguments.check )
local stripped = '<<stripped>>'

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

local indexDefinitions
local kAlbumAssetQuery

if require 'WFPlatformInfo'.isMobile() then
	indexDefinitions = 'LrmCatalogIndexDefinitions.lua'
	kAlbumAssetQuery = [[SELECT album_asset.docId, album_asset.assetId FROM CARRAY( ?, ?, 'char*' ) AS ai JOIN album_asset ON
							ai.value = album_asset.assetId]]
else
	kAlbumAssetQuery = [[SELECT album_asset_v2.docId, album_asset_v2.assetId FROM CARRAY( ?, ?, 'char*' ) AS ai JOIN album_asset_v2 ON
							ai.value = album_asset_v2.assetId]]
end

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

local continuousTaskDelays = {
	progressiveBackoff = false,
	noWorkDelay = 1,
	maxNoWorkDelay = 1,
	errorDelay = 1,
	maxDelay = 1,
}

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

local function albumAssetsForFilteredAssetsQueryFn( catalog, assetIds )

	-- This function is used to find album_assets related to assets that
	-- are blocked from upload because of an ozError.

	return catalog:indexStore():queryOnce {
		sql = kAlbumAssetQuery,
		assetIds,
		#assetIds,
	}

end

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

local function stripUpdatedSinceTime( url )

	local relativeUrl = url:replace( 'https://photos-stage.adobe.io', '')

	local updatedSinceTime = relativeUrl:match( "updated_since=(.*)" )
	if updatedSinceTime then

		relativeUrl = relativeUrl:replace( updatedSinceTime, stripped )

	end

	return relativeUrl

end

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

local function maybeAddQuickerRetriesHeader( options )

	for _, header in ipairs( options.requestHeaders or {} ) do
		if header.header and header.header:lower() == 'x-quicker-retries' then
			return
		end
	end

	options.requestHeaders = options.requestHeaders or {}

	options.requestHeaders[ #options.requestHeaders + 1 ] = {
		header = 'X-Quicker-Retries',
		value = 'true',
	}

end

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

local function makeWFCatSpecSpyWithHttpMockTracking( rxTest, catSpec, options, optLogger )

	local defaultTransform = options.defaultTransform
	local injectQuickerRetries = options.injectQuickerRetries

	local skipResponseDataCheck = false

	local function compareData( url, optArgs, traceback, mockData, ozData, transformFn )

		if skipResponseDataCheck then
			return
		end
		local mockDataAsTable = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( mockData )
		local ozDataAsTable = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( ozData )

		-- WFHttpClientImpCurl.cpp's OnResponseReceived_L() is mapping empty responses to nil
		-- whereas WFHttpClientTask.m pushResponseDataOrError does not.
		-- so if we get a nil response and are expecting an empty table set it to an empty table
		if ozDataAsTable == nil and mockDataAsTable and next(mockDataAsTable) == nil then
			ozDataAsTable = {}
		end

		-- Add same url and optArgs to make it easier to debug failures
		if mockDataAsTable then
			mockDataAsTable._DEBUG_url = url
			mockDataAsTable._DEBUG_optArgs = optArgs
			mockDataAsTable._DEBUG_traceback = traceback
		end

		if ozDataAsTable then
			ozDataAsTable._DEBUG_url = url
			ozDataAsTable._DEBUG_optArgs = optArgs
			ozDataAsTable._DEBUG_traceback = traceback
		end

		if transformFn then
			mockDataAsTable, ozDataAsTable = transformFn( mockDataAsTable, ozDataAsTable, true )
		else
			mockDataAsTable, ozDataAsTable = defaultTransform( mockDataAsTable, ozDataAsTable, true )
		end

		-- Compare the mock data to the real oz data to make sure the mocks
		-- are correct, and that we are getting what is expected from oz.
		rxTest:expectEqual( mockDataAsTable, ozDataAsTable )

	end

	local function compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )

		-- For now just compare status_code instead of entire result header.

		-- Sample header:
		--	["Access-Control-Allow-Credentials"] = "true",
		--	["Access-Control-Allow-Methods"] = "GET,HEAD,PUT,DELETE,POST,OPTIONS",
		--	["Access-Control-Max-Age"] = "60",
		--	Connection = "keep-alive",
		--	["Content-Length"] = "154",
		--	["Content-Type"] = "application/json",
		--	Date = "Mon, 20 Aug 2018 17:27:49 GMT",
		--	Server = "APIP",
		--	Status = "415 Unsupported Media Type",
		--	["X-Account-Id"] = "2e28b0705701e8fe6f7cba1c79c891e7",
		--	["X-Client-Id"] = "WFMacTest",
		--	["X-Client-Identifier"] = "Lightroom",
		--	["X-Gw-App-DevMode"] = "true",
		--	["X-Request-Id"] = "qfRKFJB89dmnqv1jSZprBB4dbDi1b0Hh",
		--	["X-Result-Code"] = "1003",
		--	["X-Result-Subtype"] = "InvalidContentTypeError",
		--	["X-Traffic-Type"] = "customer",
		--	retryCount = 2,
		--	status_code = "415",
		--	wf_time = 1.1892045750283,

		if ozResultHeaders and ( ozResultHeaders.status_code ~= mockResultHeaders.status_code ) then

			local mockResultsTable = {
				_DEBUG_url = url,
				_DEBUG_optArgs = optArgs,
				_DEBUG_traceback = traceback,
				resultHeaders = mockResultHeaders
			}

			local ozResultsTable = {
				_DEBUG_url = url,
				_DEBUG_optArgs = optArgs,
				_DEBUG_traceback = traceback,
				resultHeaders = ozResultHeaders
			}

			rxTest:expectEqual( mockResultsTable, ozResultsTable )

		end

	end

	local function put( self, url, data, optArgs )

		if injectQuickerRetries then
			-- Add here to be compatible with unit test request headers
			-- that specify this in case we remove the always add below.
			maybeAddQuickerRetriesHeader( optArgs )
		end

		local mockPutBinaryRequest = self._httpMock.put( stripUpdatedSinceTime( url ), data, optArgs )

		local notifyWhenHttpRequestIsInProgress = optArgs.notifyWhenHttpRequestIsInProgress

		if notifyWhenHttpRequestIsInProgress then
			-- When notifyWhenHttpRequestIsInProgress is set, the HTTP request will fire
			-- with a "requestInProgress" event when the HTTP request starts processing.
			-- For the mocked request, we can ignore this data event.
			mockPutBinaryRequest = mockPutBinaryRequest:skip( 1 )
		end

		return mockPutBinaryRequest
		:flatMap( function( mockData, mockResultHeaders )

			local requestNum = self._httpMock.requestCount()

			-- Oz says graceful shutdown should allow use of quicker retries on all
			-- http traffic. see https://www.pivotaltracker.com/story/show/164908026
			-- turn the option on for all tests. Add here so http mock doesn't need
			-- to specify X-Quicker-Retries for all the requests.
			maybeAddQuickerRetriesHeader( optArgs )

			local result = catSpec:put( url, data, optArgs )
			
				:map( function( ozDataOrMessage, ozResultHeaders )
				
					if notifyWhenHttpRequestIsInProgress and ozDataOrMessage == 'requestInProgress' then
						-- Because notifyWhenHttpRequestIsInProgress is set, this HTTP request must
						-- return the first event (requestInProgress) without mapping it.
						return ozDataOrMessage, ozResultHeaders
					end
				
					local ozData = ozDataOrMessage
				
					local traceback = self._httpMock.requestTraceback( requestNum )
					local transformFn = self._httpMock.getAttachedFunction( requestNum )
					compareData( url, optArgs, traceback, mockData, ozData, transformFn )

					compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )
					
					if optLogger then
						local verb = optArgs.requestMethod or 'PUT'
						optLogger:infof( "\n\n######################## %s ########################\n\n%s\n\ngot response\n\n%s\n\n",
							verb,
							WFTableArchiving.tableToString( KSJsonDecoder.decodeWebSafeJson( data ) ),
							WFTableArchiving.tableToString( KSJsonDecoder.decodeWebSafeJson( ozData ) ) )
					end

					return ozData, ozResultHeaders
					
				end )

			return result
		
-- TODO [kcarlson 2018-06-27]: handle errors

		end )
	end

	local function get( self, url, optArgs )
	
		if injectQuickerRetries then
			-- Add here to be compatible with unit test request headers
			-- that specify this in case we remove the always add below.
			maybeAddQuickerRetriesHeader( optArgs )
		end

		local originalOptArgs = KSTableUtils.deepCopy( optArgs )
		local dontCompareToMockResponse = optArgs.dontCompareToMockResponse
		optArgs.dontCompareToMockResponse = nil

		return self._httpMock.get( stripUpdatedSinceTime( url ), originalOptArgs )
		:flatMap( function( mockData, mockResultHeaders )

			local requestNum = self._httpMock.requestCount()

			-- Oz says graceful shutdown should allow use of quicker retries on all
			-- http traffic. see https://www.pivotaltracker.com/story/show/164908026
			-- turn the option on for all tests. Add here so http mock doesn't need
			-- to specify X-Quicker-Retries for all the requests.
			maybeAddQuickerRetriesHeader( optArgs )

			return catSpec:get( url, optArgs )
			:map( function( ozData, ozResultHeaders )

				if not dontCompareToMockResponse then
					local traceback = self._httpMock.requestTraceback( requestNum )
					local transformFn = self._httpMock.getAttachedFunction( requestNum )
					compareData( url, optArgs, traceback, mockData, ozData, transformFn )
					compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )
				end

				if optLogger then
					optLogger:infof( "\n\n######################## GET ########################\n\n%s\n\ngot response:\n\n%s\n\n",
						url, WFTableArchiving.tableToString( KSJsonDecoder.decodeWebSafeJson( ozData ) ) )
				end
				
				return ozData, ozResultHeaders
			end )
-- TODO [kcarlson 2018-06-27]: handle errors

		end )
	end

	local function putBinary( self, url, data, optArgs )

		local copyOfOptArgs = KSTableUtils.deepCopy( optArgs )

		local mockPutBinaryRequest = self._httpMock.put( stripUpdatedSinceTime( url ), data, copyOfOptArgs )

		local notifyWhenHttpRequestIsInProgress = optArgs.notifyWhenHttpRequestIsInProgress

		if notifyWhenHttpRequestIsInProgress then
			-- When notifyWhenHttpRequestIsInProgress is set, the HTTP request will fire
			-- with a "requestInProgress" event when the HTTP request starts processing.
			-- For the mocked request, we can ignore this data event.
			mockPutBinaryRequest = mockPutBinaryRequest:skip( 1 )
		end

		-- Issue the mocked call first since it will return results immediately
		-- and more likely in order.
		return mockPutBinaryRequest
		:catch( function( mockError, mockResultHeaders )
			return Observable.just( mockError, mockResultHeaders )
		end )
		:flatMap( function( mockData, mockResultHeaders )

			-- Get requestNum here in case two simultaneous putBinary calls
			-- return results out the order they were issued in. As we can
			-- issue an xmp_develop and master request simultaneously.
			local requestNum = self._httpMock.requestCount()
			
			return catSpec:putBinary( url, data, optArgs )
			:map( function( ozDataOrMessage, ozResultHeaders )
			
				if notifyWhenHttpRequestIsInProgress and ozDataOrMessage == 'requestInProgress' then
					-- Because notifyWhenHttpRequestIsInProgress is set, this HTTP request must
					-- return the first event (requestInProgress) without mapping it.
					return ozDataOrMessage, ozResultHeaders
				end
			
				local traceback = self._httpMock.requestTraceback( requestNum )
				compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )
				return ozDataOrMessage, ozResultHeaders
				
			end )
			:catch( function( ozErrorJson, ozResultHeaders )
				local traceback = self._httpMock.requestTraceback( requestNum )
				compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )
				return Observable.throw( ozErrorJson, ozResultHeaders )
			end )

		end )
	end

	local function getBinary( self, url, optArgs )
		return self._httpMock.get( stripUpdatedSinceTime( url ), optArgs )
		:flatMap( function( mockData, mockResultHeaders )

			local requestNum = self._httpMock.requestCount()

			return catSpec:getBinary( url, optArgs )
			:map( function( ozData, ozResultHeaders )
				local traceback = self._httpMock.requestTraceback( requestNum )
				compareResultHeaders( url, optArgs, traceback, mockResultHeaders, ozResultHeaders )
				return ozData, ozResultHeaders
			end )
-- TODO [kcarlson 2018-06-27]: handle errors

		end )
	end

	local function expectHttpRequest( self, httpRequest )

		check( 'WFCatSpec' )

		self._httpMock.addHttpRequest( httpRequest )

	end

	local WFCatSpecSpy = {
		_httpMock = WFRxHttpClientMock( rxTest, {} ),

		getBinary = getBinary,
		putBinary = putBinary,
		put = put,
		get = get,
		catalogHref = function( self, ...) return catSpec:catalogHref( ... ) end,
		catalogUpdatesUrl = function( self, ...) return catSpec:catalogUpdatesUrl( ... ) end,
		expectHttpRequest = expectHttpRequest,
		requestCount = function( self ) return self._httpMock.requestCount() end,
		wfOzUser = function( self ) return catSpec:wfOzUser() end
	}

	return Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

end

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

local function setupOzTest( rxTest, optCatalogSpecSpy, catalogSpecSpyOptions, optLogger, enableFeature )

	local catalogTestSession = WFCatTestSession( rxTest, nil, indexDefinitions )

	local catalog, catalogSpec, catalogSpecOrSpy

	rxTest:expectSingleValue( catalogTestSession:catalog()
		:map( function( _catalog )
			check( 'WFCatalog' )
			catalog = _catalog
			catalogSpec = _catalog:ozCatalogSpec()
			catalogSpecOrSpy = catalogSpec
			return _catalog
		end )
	, ':ignore:' )

	if enableFeature then
	
		rxTest:waitFor( catalog:ozUser():enableFeature( enableFeature ) )
		
	end

	rxTest:expectSingleValue( catalog:ozUser():getAccountDocFromOz(), ':ignore:' )

	if optCatalogSpecSpy then
	
		catalogSpecOrSpy = makeWFCatSpecSpyWithHttpMockTracking( rxTest, catalogSpec, catalogSpecSpyOptions, optLogger )
	
	end
	
	local conflictResolver = WFCatConflictResolver( catalog:docStore(), catalog:indexStore() )
	rxTest:addCloseTarget( conflictResolver )

	local pusher = WFCatMetadataPusher( catalog, {
			delayBeforeSendingUpdates = .01,
			continuousTaskDelays = continuousTaskDelays,
			albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		},
		catalogSpecOrSpy )

	rxTest:addCloseTarget( pusher )

	local pullerOptions = {
		continuousTaskDelays = continuousTaskDelays,
		catSpec = catalogSpecOrSpy,
		isCatalogPuller = true
	}

	local puller = WFCatMetadataPuller( catalog, "Test Puller", pullerOptions )
	rxTest:addCloseTarget( puller )

	local uploader = WFCatBinaryUploader( catalog, nil, continuousTaskDelays, catalogSpecOrSpy )

	rxTest:finally( function()

		uploader:stopContinuousUploads()
		uploader:close()

		catalog:indexStore():close()

	end )

	return catalogTestSession, catalog, catalogSpecOrSpy, pusher, puller, uploader, conflictResolver

end

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

local function setupSpaceOzTest( rxTest, memberRole, enableFeature, uploadOriginalBinary )

	local ownerCatalogTestSession = WFCatTestSession( rxTest, nil, indexDefinitions )

	-- Create the owner's catalog.

	local ownerWFCatalog, ownerCatalogSpec

	rxTest:waitFor(

		ownerCatalogTestSession:catalog()
			:map( function( _catalog )
				check( 'WFCatalog' )
				ownerWFCatalog = _catalog
			end ) )

	rxTest:waitFor(

		ownerCatalogTestSession:catalogSpec()
			:tap( function( _catalogSpec )
				check( 'WFCatSpec' )
				ownerCatalogSpec = _catalogSpec
			end ) )
		
	if enableFeature then
	
		rxTest:waitFor( ownerWFCatalog:ozUser():enableFeature( enableFeature ) )
		rxTest:waitFor( ownerWFCatalog:ozUser():getAccountDocFromOz() )
	
	end
		
	-- Create a shared album. Add an album with an asset to the catalog.
	
	-- asset document ...

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local assetDocRevId = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl = string.format( '%s/assets/%s/revisions/%s',
										  ownerCatalogSpec:catalogHref(),
										  assetId, assetDocRevId )

	local assetDocContent = KSJsonEncoder.encodeOzUploadJson {
		user_id = ownerCatalogSpec:wfOzUser():userId(),
		payload = {
			captureDate = "2004-11-02T11:10:09",
			importSource = {
				importTimestamp = '2016-01-29T15:53:04Z',
				importedBy = ownerCatalogSpec:ozUserId(),
				fileName = "fakeFile01.jpg",
				originalHeight = 16,
				originalWidth = 16,
			},
		},
		type = 'asset',
		subtype = 'image',
	}

	local assetDocPutArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectOnlyCompleted(
		ownerCatalogSpec:put( assetDocPutUrl, assetDocContent, assetDocPutArgs )
			:filter( function() return false end ) )
			
	-- upload master binary
	
	if uploadOriginalBinary then
	
		local assetOriginalPutBinaryUrl = string.format( '%s/assets/%s/revisions/%s/master',
										  ownerCatalogSpec:catalogHref(),
										  assetId, assetDocRevId )
		
		local originalMock = 'original_data'
		
		local assetOriginalPutBinaryArgs = {
			requestHeaders = {
				{ header = 'X-Generate-Renditions', value = 'all' },
				{ header = 'Content-Type', value = 'application/octet-stream' },
				{ header = "Accept", value = "application/json" },
			},
		}
		
		rxTest:waitFor( ownerCatalogSpec:putBinary( assetOriginalPutBinaryUrl, originalMock, assetOriginalPutBinaryArgs ) )
	
	end 

	-- album doc ...

	local albumId = KSIdGenerator.uuidInLowercaseHex()

	local albumDocPutUrl = string.format( '%s/albums/%s',
										  ownerCatalogSpec:catalogHref(),
										  albumId )

	local albumDocContent = KSJsonEncoder.encodeOzUploadJson {
		subtype = 'collection',
		payload = {
			name = "WFCatOzTestSession space album",
			
		},
	}

	local albumDocPutArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectOnlyCompleted(
		ownerCatalogSpec:put( albumDocPutUrl, albumDocContent, albumDocPutArgs )
			:filter( function() return false end ) )

	-- album_asset doc ...

	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()

	local albumAssetDocPutUrl = string.format( '%s/albums/%s/assets',
											  ownerCatalogSpec:catalogHref(),
											  albumId )

	local albumAssetDocContent = KSJsonEncoder.encodeOzUploadJson {
		resources = {
			{
				id = assetId,
				album_asset_id = albumAssetId,
			},
		},
	}

	local albumAssetDocPutArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectOnlyCompleted(
		ownerCatalogSpec:put( albumAssetDocPutUrl, albumAssetDocContent, albumAssetDocPutArgs )
			:filter( function() return false end ) )
	
	-- Create the shared space.
	
	local attributes = {
	
		private = true,
		allowInviteRequests = true,
	
		download = false,
		metadata = false,
		location = false,
		pickedOnly = false,
		ratingAtLeast = 0,
		comments_disabled = false,
		favorites_disabled = false,
		hide_url = false,
		
		author = "Edvard Munch",
		title = "The Scream",
		description = "Anyone can look at The Scream and feel something",
		showTitle = true,
		showAuthor = true,
		showCaptions = true,
		showCover = true,
		themeId = "Single",
		styleId = "Light",
		presentationFilters = true,
		
	}
	
	local spaceId
	
	rxTest:expectSingleValue( WFCatCollaboration.shareAlbum( ownerWFCatalog, albumId, attributes )
		:map( function( shareAttributes )
		
			spaceId = shareAttributes.id
			shareAttributes.id = nil
			shareAttributes.url = nil
			return shareAttributes
			
		end ),
		attributes )
	
	-- Verify the space exists and is synced.
	
	rxTest:expectSingleValue( ownerWFCatalog:docStore()
		:docValue( spaceId )
		:map( function( docValue )
		
			return not not docValue._serverProvidedMetadata
		
		end ), true )
	
	-- Now that a shared album/space exists that allow invites, create the member!
	
	local memberOptions = {
		testName = 'spaceFeedManagerMember1'
			-- Specify a custom test name here; this allows us to fetch a second
			-- account from the Oz account server. Otherwise, the account server
			-- will return the same account we previoulsy fetched for this
			-- same test case.
	}
	
	local memberCatalogTestSession = WFCatTestSession( rxTest, nil, indexDefinitions, memberOptions )

	local memberWFCatalog, memberCatalogSpec

	rxTest:waitFor(

		memberCatalogTestSession:catalog()
			:tap( function( _catalog )
				check( 'WFCatalog' )
				memberWFCatalog = _catalog
			end ) )
	
	rxTest:waitFor(

		memberCatalogTestSession:catalogSpec()
			:tap( function( _catalogSpec )
				check( 'WFCatSpec' )
				memberCatalogSpec = _catalogSpec
			end ) )
	
	-- Add the member to the album.
	
	rxTest:expectOnlyCompleted( ownerCatalogTestSession:reloadAccessAndRefreshTokens() )
		-- Make sure we're using the album owners access tokens for the following.
		
	local newSpaceUserId = KSIdGenerator.uuidInLowercaseHex()
	
	local requestAccessUrl = string.format( '%s/spaces/%s/users/%s',
											  ownerCatalogSpec:catalogHref(),
											  spaceId,
											  newSpaceUserId )

	local requestAccessContent = KSJsonEncoder.encodeOzUploadJson {
		account_id = memberWFCatalog:ozUserId(),
		invite_id = KSIdGenerator.uuidInLowercaseHex(),
		payload = {
			status = 'accepted',
			role = memberRole,
			userCreated = KSDate.timeInIso8601Format(),
			userUpdated= KSDate.timeInIso8601Format()
		}
	}

	local requestAccessArgs = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectSingleValue(
		ownerCatalogSpec:put( requestAccessUrl, requestAccessContent, requestAccessArgs )
			:map( function( data, headers )
			
				return headers.status_code == '201'
				
			end ), true )
	
	rxTest:expectOnlyCompleted( memberCatalogTestSession:reloadAccessAndRefreshTokens() )
		-- reload the member access token.
		
	return ownerWFCatalog, ownerCatalogSpec, ownerCatalogTestSession,
			memberWFCatalog, memberCatalogSpec, memberCatalogTestSession,
			spaceId, newSpaceUserId, albumId, assetId

end

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

return {
	setupOzTest = setupOzTest,
	setupSpaceOzTest = setupSpaceOzTest,
	stripUpdatedSinceTime = stripUpdatedSinceTime,
}
