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

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.self_test
local WFTestFileUtils = require 'WFTestFileUtils'

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local KSTableUtils = require 'KSTableUtils'
local WFCrypto = require 'WFCrypto'
local WFPlatformInfo = require 'WFPlatformInfo'
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'

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

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

	-- WF.oz_client.managed_catalog
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
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 albumAssetsForFilteredAssetsQueryFnForCatalog( 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 )

	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 )

		if WFPlatformInfo.isiOS then

			-- WFHttpClientTask.m pushResponseDataOrError returns empty responses as tables,
			-- so the mock implementation must do that as well.

			mockDataAsTable = mockDataAsTable or {}

		end

		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 options.logger then
						local verb = optArgs.requestMethod or 'PUT'
						options.logger: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 ignoreMockResponse = optArgs.ignoreMockResponse
		optArgs.ignoreMockResponse = 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 ignoreMockResponse and ozData ~= 'requestInProgress' 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 options.logger then
					options.logger: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,
			{},    -- expectedRequestResponsePairs
			false, -- optRateLimiter
			true   -- enableIgnoreMockResponse
		),

		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, options )

	options = options or {}

	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 options.enableFeature then

		rxTest:waitFor( catalog:ozUser():enableFeature( options.enableFeature ) )

	end

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

	if options.catalogSpecSpy then

		catalogSpecOrSpy = makeWFCatSpecSpyWithHttpMockTracking( rxTest, catalogSpec, options )

	end

	local conflictResolver = WFCatConflictResolver( catalog:docStore(), catalog:indexStore() )
	rxTest:addCloseTarget( conflictResolver )

	local capabilities = {
		[ 'WFCatBinaryUploader.atomicDevelopSettings' ] = options.atomicDevelopSettings,
	}

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

	rxTest:addCloseTarget( pusher )

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

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

	local uploaderOptions = {
		fallbackToOzUploadForOriginalsOnly = true,
		capabilities = capabilities,
	}

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

	rxTest:finally( function()

		uploader:stopContinuousUploads()
		uploader:close()

		catalog:indexStore():close()

	end )

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

end

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

local function getAssetPayload( rxTest, catalogSpec, isVideo )

	if isVideo then

		local forceCreateNewTestFolder = true
		local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest, nil, forceCreateNewTestFolder )
		local fileName = 'IMG_7772.mp4'

		local originalFilePath = WFCatAssetTestUtils.getTestDataOriginalFilePath( rxTest, tempTestFolder, fileName )
		local sha256 = WFCrypto.digestFile( originalFilePath, 'SHA256' )

		local payload = WFCatAssetTestUtils.createBaseAssetDocument( nil, nil, nil, sha256, originalFilePath, catalogSpec:ozUserId() )

		payload._id = nil
		payload._newRevId = nil
		payload.type = nil
		payload.subtype = nil
		
		return payload

	else

		return {
			captureDate = "2004-11-02T11:10:09",
			importSource = {
				importTimestamp = '2016-01-29T15:53:04Z',
				importedBy = catalogSpec:ozUserId(),
				fileName = "fakeFile01.jpg",
				originalHeight = 16,
				originalWidth = 16,
			}
		}
		
	end

end

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

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

	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 = getAssetPayload( rxTest, ownerCatalogSpec, useVideo ),
		type = 'asset',
		subtype = useVideo and 'video' or '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 originalMockOrFilePath = 'original_data'

		if useVideo then

			-- originalFile is copied to tempTestFoldr at getAssetPayload function
			local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
			originalMockOrFilePath = WFCatAssetTestUtils.getTestDataOriginalFilePath( rxTest, tempTestFolder, 'IMG_7772.mp4' )

		end

		local assetOriginalPutBinaryArgs = {
			requestHeaders = {
				{ header = 'Content-Type', value = useVideo and 'video/mp4' or 'application/octet-stream' },
				{ header = "Accept", value = "application/json" },
			},
			dataFormat = useVideo and 'filePath' or nil
		}

		if not doNotGenerateRenditions then

			local requestHeaders = assetOriginalPutBinaryArgs.requestHeaders
			requestHeaders[ #requestHeaders + 1 ] = { header = 'X-Generate-Renditions', value = 'all' }

		end

		rxTest:waitFor( ownerCatalogSpec:putBinary( assetOriginalPutBinaryUrl, originalMockOrFilePath, 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,
		
		invitationSharingDisallowed = false,
		invitationSharingAllowDomains = false,
		publicSharingDisallowed = false,
		requestAccessSharingDisallowed = false,
		anyoneCanUploadDisallowed = false,

		feedbackDisallowed = false,
		communityPostingDisallowed = false

	}

	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

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

local function albumAssetsForFilteredAssetsQueryFnForSpaceMgr( catalog, assetIds )

	if WFPlatformInfo.isMobile() then

		return catalog:indexStore():queryOnce {
					sql = [[SELECT album_asset.docId, album_asset.assetId FROM CARRAY( ?, ?, 'char*' ) as ai JOIN album_asset ON
								ai.value = album_asset.assetId]],
					assetIds,
					#assetIds,
			}

	else

		return catalog:indexStore():queryOnce {
				sql = [[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]],
				assetIds,
				#assetIds,
			}

	end

end

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

local function createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedsManagerArgs  )

	spaceFeedsManagerArgs = spaceFeedsManagerArgs or {}

	spaceFeedsManagerArgs = KSTableUtils.merge( {}, {
			taskDelays = {
				progressiveBackoff = false,
				noWorkDelay = 0.5,
				maxNoWorkDelay = 1,
				errorDelay = 1,
				maxDelay = 1,
			},
			albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFnForSpaceMgr,
			testMode = true,
		},
		spaceFeedsManagerArgs )

	local spaceFeedsManager = require 'WFCatSpaceFeedsManager'( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )

	return spaceFeedsManager

end

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

return {
	createSpaceFeedsManager = createSpaceFeedsManager,
	setupOzTest = setupOzTest,
	setupSpaceOzTest = setupSpaceOzTest,
	stripUpdatedSinceTime = stripUpdatedSinceTime,
	DEBUG_getAssetPayload = getAssetPayload
}
