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

WFCatAlbumsTest.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.

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

local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.self_test
local WFRxTestCase = require 'WFRxTestCase'

	-- WF.core.os
local WFPlatformInfo = require 'WFPlatformInfo'
	
	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local _ = require 'WFArguments_uuid'

	-- WF.oz_client.managed_catalog.search
local WFCatSearchQuery = require 'WFCatSearchQuery'

	-- WF.oz_client.managed_catalog
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatSpecMock = require 'WFCatSpecMock'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'

local WFModelsUtils = require 'WFModelsUtils'
local WFOzTestUtils = require 'WFOzTestUtils'
local WFCatTestSession = require 'WFCatTestSession'

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

local check = assert( WFArguments.check )

local createFakeWFCatalog = WFCatMockCatalog.createFakeWFCatalog

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

return function( optSubmodelsByImplementation, maybeExistingTestClass )
	
	----------------------------------------------------------------------------
	
	local function callFunctionOrSubmodel( rxTest, wfCatalog, fnNamespace, fnName, ... )

		return WFModelsUtils.callFunctionOrSubmodel( rxTest, optSubmodelsByImplementation, wfCatalog, fnNamespace, fnName, ... )
	
	end
	
	----------------------------------------------------------------------------

	local WFCatAlbumsTest = maybeExistingTestClass or {}

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

	local function makeAdderOfAlbum( rxTest, wfCatalog )

		local counter = 9

		return function( parentId, hidden, importSource, albumId )

			local docId
			local albumName = string.format( "Album %d", counter )

			rxTest:expectOnlyCompleted(
			
				callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','createAlbum', wfCatalog, albumName, parentId, hidden, importSource, albumId )
					:tap( function( localAlbumId, globalAlbumId )
						check( 'number', 'uuid' )
						docId = globalAlbumId
					end )
					:takeLast( 0 )
				
			)

			counter = counter - 1

			return docId

		end

	end
	
	----------------------------------------------------------------------------
	
	local function makeAdderOfSmartAlbum( rxTest, wfCatalog )
		
		local counter = 9

		return function( parentId, predicates, albumId )
			
			local docId
			local albumName = string.format( "Smart Album %d", counter )
			
			rxTest:expectOnlyCompleted(
			
				callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','createSmartAlbum', wfCatalog, albumName, parentId, predicates, albumId )
					:tap( function( localAlbumId, globalAlbumId )
						check( 'number', 'uuid' )
						docId = globalAlbumId
					end )
					:takeLast( 0 )
				
			)

			counter = counter - 1

			return docId

		end
		
	end
	
	----------------------------------------------------------------------------

	local function makeAdderOfFolder( rxTest, wfCatalog )

		local counter = 9

		return function( parentId, importSource, folderId )

			local docId
			local folderName = string.format( "Folder %d", counter )

			rxTest:expectOnlyCompleted(

				callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','createFolder', wfCatalog, folderName, parentId, importSource, folderId )
					:tap( function( localFolderId, globalFolderId )
						check( 'number', 'uuid' )
						docId = globalFolderId
					end )
					:takeLast( 0 )
			)

			counter = counter - 1

			return docId

		end

	end


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

	local function makeSearcher( rxTest, docStore, indexStore, searchSchema )

		return function( predicates )

			local terms = WFCatSearchQuery.createQueryParamsStream( searchSchema, {
				predicates = predicates
			} )

			return WFCatSearchQuery.privateUnitTestExports.uncalmedResultSetQuery( 
				indexStore,
				[[SELECT docId FROM assets @JOIN ORDER BY height DESC]],
				terms,
				'assets.docId'
			)

		end

	end

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

	local function sanitizeAndSortAlbumsList( albums, isSyncTest )
		
		albums = albums or {}
		
		albums = KSTableUtils.deepCopy( albums )
		
		local now = KSDate.timeInIso8601Format()
		
		for _, album in ipairs( albums ) do
		
			if not isSyncTest then
				if type( album.fullDocId ) == 'string' and #album.fullDocId == 32 then
					album.fullDocId = 'uuid'
				end
			end
			
			album.isMarkedForOffline = nil
			
			if not isSyncTest then
				album.syncStatus = nil
			end
			
			if KSDate.differenceInSeconds( album.createdTimestamp, now ) < 100 then
				album.createdTimestamp = 'recently'
			end

			if KSDate.differenceInSeconds( album.updateTimestamp, now ) < 100 then
				album.updateTimestamp = 'recently'
			end
		end
		
		table.sort( albums, function( a, b )
				return a.id < b.id
			end )
		return albums
	end

	----------------------------------------------------------------------------
	
	local function setupMockedCatalog( rxTest, catalogId, urlResponses )

		local userId = KSIdGenerator.uuidInLowercaseHex()
	
		local indexSpecScript
		if WFPlatformInfo.isMobile() then
			indexSpecScript = 'LrmCatalogIndexDefinitions.lua'
		end
	
		local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, urlResponses )
		local wfCatalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec, nil, indexSpecScript, nil )
	
		rxTest:finally( function()
			wfCatalog:indexStore():close()
		end )
	
		return wfCatalog, catalogSpec

	end
	
	----------------------------------------------------------------------------

	local function setupOzCatalog( rxTest )

		local indexDefinitions
		if WFPlatformInfo.isMobile() then
			indexDefinitions = 'LrmCatalogIndexDefinitions.lua'
		end
	
		local wfCatalog, catalogSpec

		local catalogTestSession = WFCatTestSession( rxTest, nil, indexDefinitions )
		
		rxTest:waitFor( catalogTestSession:catalog()
			:map( function( _catalog )
				check( 'WFCatalog' )
				wfCatalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				return _catalog
			end ) )
		
		rxTest:finally( function()
			wfCatalog:indexStore():close()
		end )
	
		return wfCatalog, catalogSpec

	end

	----------------------------------------------------------------------------
	
	local cachedAlbumId = "bogus"

	local function createAlbumExpectedRequestResponses( options )

		options = options or {}
		local responses = {}

		if not options.putError then

			responses[ #responses + 1 ] = {
				url = "url", -- "/v2/catalogs/<catalogId>/albums/<albumId>"
				data = "data",
				options = {
					requestHeaders = {
						{ header = "Content-Type", value = "application/json" }
					},
					requestMethod = "PUT",
				},
				dataFilter = function( data ) return "data" end,
				urlFilter = function( url ) return "url" end,
				response = "none",
				headers = {
					status_code = '201',
				}
			}

		else

			responses[ #responses + 1 ] = {
				url = "url", -- "/v2/catalogs/<catalogId>/albums/<albumId>"
				data = "data",
				options = {
					requestHeaders = {
						{ header = "Content-Type", value = "application/json" }
					},
					requestMethod = "PUT",
				},
				dataFilter = function( data ) return "data" end,
				urlFilter = function( url ) return "url" end,
				response = [[
					{
						"errors": {
							"body": [
								"must be valid JSON"
							]
						},
						"code": 1005,
						"description": "Input Validation Error"
					}
				]],
				headers = {
					status_code = '400',
				}
			}

			return responses

		end

		if not options.getError then

			responses[ #responses + 1 ] = {
				url = "url", -- "/v2/catalogs/<catalogId>/albums/<albumId>"
				data = "data",
				options = {},
				dataFilter = function( data ) return "data" end,
				urlFilter = function( url ) 
					cachedAlbumId = string.match( url, "albums/(.+)" )
					return "url" 
				end,
				response = [[
					{
						"base": "<base_url>/v2/catalogs/<catalog_id>/",
						"id": "<albumId>",
						"type": "album",
						"subtype": "collection",
						"created": "<created_date>",
						"updated": "<updated_date>",
						"revision_ids": [
							"<album_revision_id1>"
						],
						"payload": {
							"name": "<albumName>",
							"hideInOrganize": true
						},
						"links": {
							"self": {
								"href": "albums/<albumId>"
							}
						}
					}
				]],
				headers = {
					status_code = '200',
				},
	
				responseFilter = function( response )
					response = response:gsub( '<albumId>', cachedAlbumId )
					return response
				end,
			}

		else

			responses[ #responses + 1 ] = {
				url = "url", -- "/v2/catalogs/<catalogId>/albums/<albumId>"
				data = "data",
				options = {},
				dataFilter = function( data ) return "data" end,
				urlFilter = function( url ) return "url" end,
				response = [[
					{
						"errors": {
							"body": [
								"must be valid JSON"
							]
						},
						"code": 1005,
						"description": "Input Validation Error"
					}
				]],
				headers = {
					status_code = '400',
				}
			}

		end

		return responses

	end

	----------------------------------------------------------------------------
	
	local function testCreateAlbumSynchronously( rxTest, options )

		options = options or {}

		rxTest:setTimeLimit( options.mock and 2 or 60 )

		local catalogId = KSIdGenerator.uuidInLowercaseHex()

		local urlResponses = createAlbumExpectedRequestResponses( options )

		local wfCatalog
		
		if options.mock then
			wfCatalog = setupMockedCatalog( rxTest, catalogId, urlResponses )
		else
			wfCatalog = setupOzCatalog( rxTest )
		end

		local albumName = "adhocAlbum"
		local parentId = nil
		local hidden = true
		local albumDocId, didCreateWithOz

		rxTest:expectOnlyCompleted(
			
			callFunctionOrSubmodel(
				rxTest, wfCatalog,
				'WFCatAlbums','createAlbumSynchronously', wfCatalog, albumName, parentId, hidden )
				:tap( function( localAlbumId, globalAlbumId, _, createWithOz )
					check( 'number', 'uuid' )
					albumDocId = globalAlbumId
					didCreateWithOz = not not createWithOz
				end )
				:takeLast( 0 )
			
		)

		rxTest:expectEqual( didCreateWithOz, ( not ( options.putError or options.getError ) ) )

		rxTest:expectSingleValue( wfCatalog:docStore():docValue( albumDocId )
			:map( function( albumDocValue )
				local isOnOz = ( albumDocValue._serverProvidedMetadata ~= nil )
				local labels = albumDocValue._labels
				local hasOzLabel = labels and ( labels.oz == true ) or false

				return albumDocValue.subtype == "collection" 
					and albumDocValue.hideInOrganize == true
					and didCreateWithOz == isOnOz
					and didCreateWithOz == hasOzLabel
			end ),
			true )

	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestCreateAlbumSynchronously( rxTest )
		
		testCreateAlbumSynchronously( rxTest, { mock = true } )
	
	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestCreateAlbumSynchronously_putRequestError( rxTest )

		testCreateAlbumSynchronously( rxTest, { mock = true, putError = true } )

	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestCreateAlbumSynchronously_getRequestError( rxTest )

		testCreateAlbumSynchronously( rxTest, { mock = true, getError = true } )

	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestCreateAlbumSynchronously_testNightly( rxTest )

		testCreateAlbumSynchronously( rxTest )

	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestAddAssets( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()
		local searchSchema = wfCatalog:searchSchema()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		addAlbumFn()
		addAssetFn( '2001' )
		addAssetFn( '2002' )
		addAssetFn( '2003' )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, 1, { 2, 3, 4 } )
		)
					

		local runQueryOfTermsFromSavedSearch = makeSearcher( rxTest, docStore, indexStore, searchSchema )

		rxTest:observe( runQueryOfTermsFromSavedSearch {
			type = 'album',
			subtype = 'collection',
			album = 1,
			operator = '==',
		} )
		:expectDataWithinTime( 1, { { 4 }, { 3 }, { 2 } } )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','removeAssetsFromAlbum', wfCatalog, 1, { 2, 3 } )
		)

		rxTest:observe( indexStore:query {
			sql = [[SELECT assetId FROM album_asset_v2]],
			quiesceUntilUpToDate = true,
		} )
		:expectData { { '00000000000000000000000000000003' } }
			-- localId 4 is remaining, and that asset's uuid is 00...03

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 1 )
		)

		rxTest:observe( indexStore:query {
			sql = [[SELECT docId FROM album_asset_v2]],
			quiesceUntilUpToDate = true,
		} )
		:expectData {}

	end

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

	function WFCatAlbumsTest.rxTestAddAssetsToAlbumGivenDocIds( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		local albumId = addAlbumFn()
		local assetId1 = addAssetFn( '2001' )
		local assetId2 = addAssetFn( '2002' )
		local assetId3 = addAssetFn( '2003' )
		local assetId4 = addAssetFn( '2004' )
		local assetId5 = addAssetFn( '2004' )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId, { assetId1, assetId2, assetId5 } )
		)
		
		local album_asset_id1, album_asset_id2, album_asset_id5
		
		rxTest:expectSingleValue(
			indexStore:query( {
				sql = [[SELECT docId FROM album_asset_v2]],
				quiesceUntilUpToDate = true,
			} )
			:take( 1 )
			:flatMap( function( album_assets )
				assert( #album_assets == 3 )
				
				local album_asset_id_list = {
					album_assets[ 1 ][ 1 ],
					album_assets[ 2 ][ 1 ],
					album_assets[ 3 ][ 1 ],
				}
				
				return docStore:bulkDocValue( album_asset_id_list )
					:tap( function( docValues )
						
						for _, docValue in ipairs( docValues ) do
							if docValue.asset.id == assetId1 then
								album_asset_id1 = docValue._id
							elseif docValue.asset.id == assetId2 then
								album_asset_id2 = docValue._id
							elseif docValue.asset.id == assetId5 then
								album_asset_id5 = docValue._id
							end
							
						end
					end )
			end )
		, ':ignore:' )
		
		
		-- Sabotage the doc value of album_asset_id2
		rxTest:expectSingleValue(
			docStore:applyOverlay( {
				_id = album_asset_id2,
				asset = {
					id = '< invalid >',
				}
			} )
		, ':ignore:' )
		
		-- delete album_asset_id5
		rxTest:expectSingleValue(
			docStore:deleteDoc( album_asset_id5 )
		, ':ignore:' )
		
		local album_asset_id3 = KSIdGenerator.uuidInLowercaseHex()
		
		local album_assets = {
			assetId1,
			assetId2,
			assetId3,
			assetId4,
			assetId5,
		}
		
		local album_asset_info = {
			[ assetId2 ] = album_asset_id2,
			[ assetId3 ] = album_asset_id3,
			[ assetId5 ] = album_asset_id5,
		}
		
		local newUUIDMarker = "<new UUID>"
		
		local expectedResult = {
			[ assetId1 ] = album_asset_id1,
			[ assetId2 ] = false,
			[ assetId3 ] = album_asset_id3,
			[ assetId5 ] = newUUIDMarker,
				-- deleted doc. Should create a new doc id.
		}
		
		local album_asset_id4
		
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbumGivenDocIds', wfCatalog, albumId, album_assets, album_asset_info )
				:map( function( results )
				
					-- Removing the new UUID generated for
					-- ease of comparison
					album_asset_id4 = results[ assetId4 ]
					results[ assetId4 ] = nil
					
					-- Replace the value of the deleted album-asset
					-- if the id is different from the provided one
					results[ assetId5 ] = album_asset_id5 == results[ assetId5 ] 
						and album_asset_id5 -- provided id
						or newUUIDMarker	-- new id
					
					return results
				end )
		, expectedResult )
		
		
		local expectedDocValue = {
			type = 'album_asset',
			_id = album_asset_id4,
			album = {
				id = albumId,
			},
			asset = {
				id = assetId4,
			},
		}
		
		rxTest:expectSingleValue(
			docStore:docValue( album_asset_id4 )
				:map( function( docValue )
					return {
						type = docValue.type,
						_id = docValue._id,
						album = docValue.album,
						asset = docValue.asset,
						order = docValue.order,
					}
				end )
		, expectedDocValue )

	end

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

	function WFCatAlbumsTest.rxTestAddAssetsToAlbumGivenDocIdsSortOrder( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		local albumId = addAlbumFn()
		local assetId1 = addAssetFn( '2001' )
		local assetId2 = addAssetFn( '2002' )
		local assetId3 = addAssetFn( '2003' )
		local assetId4 = addAssetFn( '2004' )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId, { assetId1 } )
		)
		
		
		local album_asset_id1
		
		rxTest:expectSingleValue(
			indexStore:query( {
				sql = [[SELECT docId FROM album_asset_v2]],
				quiesceUntilUpToDate = true,
			} )
			:take( 1 )
			:flatMap( function( album_assets )
				assert( #album_assets == 1 )
				
				return docStore:docValue( album_assets[ 1 ][ 1 ] )
					:tap( function( docValue )
						album_asset_id1 = docValue._id
					end )
			end )
		, ':ignore:' )
		
		
		local album_asset_id2 = KSIdGenerator.uuidInLowercaseHex()
		
		local album_assets = {
			assetId1,
			assetId2,
			assetId3,
		}
		
		local album_asset_info = {
			[ assetId2 ] = album_asset_id2,
		}
		
		local expectedResult = {
			[ assetId1 ] = album_asset_id1,
			[ assetId2 ] = album_asset_id2,
		}
		
		local album_asset_id3
		
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbumGivenDocIds', wfCatalog, albumId, album_assets, album_asset_info, true )
				:map( function( results )
					album_asset_id3 = results[ assetId3 ]
					results[ assetId3 ] = nil
					return results
				end )
		, expectedResult )
		
		
		rxTest:expectSingleValue(
			docStore:bulkDocValue( { album_asset_id1, album_asset_id2, album_asset_id3 } )
				:map( function( docValues )
					
					local sortOrderOfAsset1 = docValues[ 1 ].order
					local sortOrderOfAsset2 = docValues[ 2 ].order
					local sortOrderOfAsset3 = docValues[ 3 ].order
					
					return sortOrderOfAsset1 < sortOrderOfAsset2
						and sortOrderOfAsset2 < sortOrderOfAsset3
				end )
		, true )
		
		-- Add one more asset with order.
		-- Verify if its sortOrder is greater than the last one
		-- with order
		
		local album_asset_id4
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbumGivenDocIds', wfCatalog, albumId, { assetId4 }, {}, true )
				:tap( function( results )
					album_asset_id4 = results[ assetId4 ]
				end )
		, ':ignore:' )
		
		
		rxTest:expectSingleValue(
			docStore:bulkDocValue( { album_asset_id3, album_asset_id4 } )
				:map( function( docValues )
					
					local sortOrderOfAsset3 = docValues[ 1 ].order
					local sortOrderOfAsset4 = docValues[ 2 ].order
					
					return sortOrderOfAsset3 < sortOrderOfAsset4
				end )
		, true )

	end
	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestGetAlbumAssetIdFromAlbumIdAssetId( rxTest )
	
		rxTest:setTimeLimit( 5 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		local albumId = addAlbumFn()
		local assetId1 = addAssetFn( '2001' )
		local assetId2 = addAssetFn( '2002' )
		local assetId3 = addAssetFn( '2003' )
		
		local assetIds = { assetId1, assetId2, assetId3 }
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId, assetIds )
		)

		local albumAssetIds = {}

		rxTest:expectSingleValue(
			indexStore:queryOnce {
				sql = [[ SELECT docId FROM album_asset_v2 
						 WHERE albumId = ? AND assetId IN CARRAY( ?, ?, 'char*' ) ]],
				quiesceUntilUpToDate = true,
				albumId,
				assetIds,
				#assetIds
			} 
			:map( function( rows )
				assert( #rows == 3 )
				albumAssetIds = {
					rows[ 1 ][ 1 ],
					rows[ 2 ][ 1 ],
					rows[ 3 ][ 1 ],
				}
				return albumAssetIds
			end )
			
			, ":ignore:" )
		
		local expectedFullAlbumAssetIds = { }
		rxTest:waitFor( docStore:bulkLocalIdToDocIdAsMap( albumAssetIds )
						:map( function( res ) 
							for localId, docId in pairs( res ) do
								expectedFullAlbumAssetIds [ #expectedFullAlbumAssetIds + 1 ] = docId
							end
							return expectedFullAlbumAssetIds 
						end) )
			
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
						rxTest, wfCatalog,
						'WFCatAlbums','getAlbumAssetIdFromAlbumIdAssetId', wfCatalog, albumId, assetIds )
					:map( function( results )
						return KSTableUtils.deepEquals( expectedFullAlbumAssetIds, results )
					end )
			,  true )
		
	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestGetAlbumIdAssetIdFromAlbumAssetId( rxTest )
	
		rxTest:setTimeLimit( 5 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		local albumId = addAlbumFn()
		local assetId1 = addAssetFn( '2001' )
		local assetId2 = addAssetFn( '2002' )
		local assetId3 = addAssetFn( '2003' )
		
		local expectedAlbumIdAssetIdAsMap = { 
			[ albumId ] = { 
				assetId1, assetId2, assetId3
			}
		}
		
		local assetIds = { assetId1, assetId2, assetId3 }
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId, assetIds )
		)
		
		local albumAssetIds = {}

		rxTest:expectSingleValue(
			indexStore:queryOnce {
				sql = [[ SELECT docId FROM album_asset_v2 
						 WHERE albumId = ? AND assetId IN CARRAY( ?, ?, 'char*' ) ]],
				quiesceUntilUpToDate = true,
				albumId,
				assetIds,
				#assetIds
			} 
			:map( function( rows )
				assert( #rows == 3 )
				albumAssetIds = {
					rows[ 1 ][ 1 ],
					rows[ 2 ][ 1 ],
					rows[ 3 ][ 1 ],
				}
				return albumAssetIds
			end )
			
			, ":ignore:" )
			

		
		local fullDocIds = {}
	
		rxTest:waitFor( 
			docStore:bulkLocalIdToDocIdAsMap( albumAssetIds )
			 :map( function ( res )
				for localId, docId in pairs( res ) do
					fullDocIds [ #fullDocIds + 1 ] = docId
				end
				return res 
			end ) )
		
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
						rxTest, wfCatalog,
						'WFCatAlbums','getAlbumIdAssetIdFromAlbumAssetId', wfCatalog, fullDocIds )
					:map( function( results )
						return KSTableUtils.deepEquals( expectedAlbumIdAssetIdAsMap, results )
					end )
			,  true )
		
	end

	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestupdateAlbumAssetOrdering( rxTest )
	
		rxTest:setTimeLimit( 5 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local nextHeight = 1
		local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set capture date to what we want
				baseDocValue.captureDate = date
				
				-- set the height for the query ordering
				baseDocValue.importSource.originalHeight = nextHeight
				nextHeight = nextHeight + 1
				
				return baseDocValue
			end )

		local albumId = addAlbumFn()
		local assetId1 = addAssetFn( '2001' )
		local assetId2 = addAssetFn( '2002' )
		local assetId3 = addAssetFn( '2003' )
		
		local assetIds = { assetId1, assetId2, assetId3 }
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId, assetIds )
		)
		
		local albumAssetIds = {}

		rxTest:waitFor(
			indexStore:queryOnce {
				sql = [[ SELECT docId FROM album_asset_v2 
						 WHERE albumId = ? AND assetId IN CARRAY( ?, ?, 'char*' ) ]],
				quiesceUntilUpToDate = true,
				albumId,
				assetIds,
				#assetIds
			} 
			:map( function( rows )
				assert( #rows == 3 )
				albumAssetIds = {
					rows[ 1 ][ 1 ],
					rows[ 2 ][ 1 ],
					rows[ 3 ][ 1 ],
				}
				return albumAssetIds
			end ) )
		
		local newSortOrder = { 'V', 'W', 'S' }
		
		rxTest:expectSingleValue(
			callFunctionOrSubmodel(
						rxTest, wfCatalog,
						'WFCatAlbums','updateAlbumAssetOrdering', wfCatalog, albumAssetIds , newSortOrder )
			,  true )
		
		local actualSortOrder 
		rxTest:expectSingleValue(
			indexStore:queryOnce {
				sql = [[ SELECT sortOrder FROM album_asset_v2 
						 WHERE albumId = ? AND assetId IN CARRAY( ?, ?, 'char*' ) ]],
				quiesceUntilUpToDate = true,
				albumId,
				assetIds,
				#assetIds
			} 
			:map( function( rows )
				actualSortOrder = {
					rows[ 1 ][ 1 ], rows[ 2 ][ 1 ], rows[ 3 ][ 1 ],
				}
			
				return actualSortOrder
				
			end)
			
		, ":ignore:" )
		table.sort( actualSortOrder )
		table.sort( newSortOrder )
		
		rxTest:expectEqual( true, KSTableUtils.deepEquals( actualSortOrder, newSortOrder ) )
		
	end
	
	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestBreakCycles( rxTest )
		
		local wfCatalog = createFakeWFCatalog( rxTest )
		local rows = {
			{ 'a', 1, 0, 0, '2001', '2002', 'a' },
		}
		
		callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','_breakCycles', rows )
		
		rxTest:expectEqual( {
			{ 'a', 1, 0, 0, '2001', '2002', nil },
		}, rows )
		
		rows = {
			{ 'a', 1, 0, 0, '2001', '2002', 'b' },
			{ 'b', 2, 0, 0, '2003', '2004', 'a' },
		}

		callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','_breakCycles', rows )

		rxTest:expectEqual( {
			{ 'a', 1, 0, 0, '2001', '2002', nil },
			{ 'b', 2, 0, 0, '2003', '2004', 'a' },
		}, rows )

	end

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

	function WFCatAlbumsTest.rxTestCannotReparentOntoSelf( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		addAlbumFn()
		rxTest:expectOnlyError(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentAlbumOrFolder', wfCatalog, 1, 1 ),
			'InvalidParent'
		)
	end

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

	function WFCatAlbumsTest.rxTestAlbumHierarchy( rxTest )
		
		rxTest:setTimeLimit( 60 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )
		
		local addFolderFn = makeAdderOfFolder( rxTest, wfCatalog )
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		
		-- create a folder
		addFolderFn()
		
		local folderHierarchy = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )
		
		folderHierarchy:expectDataWithinTime( 3, {
				{
					id = 1,
					name = "Folder 9",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				} } )
		
		-- create a folder in a folder
		addFolderFn( 1 )
		
		local folderWithNestedFolder =  {
				{
					id = 1,
					name = "Folder 9",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				},
				{
					id = 2,
					parentId = 1,
					name = "Folder 8",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				},
			}
			
		folderHierarchy:expectDataWithinTime( 3, folderWithNestedFolder )
		
		rxTest:sleep( 1 )
		folderHierarchy:expectDataWithinTime( 3, folderWithNestedFolder )
			
		-- create a loop of folders (failure)
		rxTest:expectOnlyError( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentAlbumOrFolder', wfCatalog, 1, 2 ), "CycleDetected" )
		
		-- put an album in a folder (ok )
		addAlbumFn( 1 )
		addAlbumFn( 2 )
		
		local foldersWithNestedAlbums = { 
			{
				id = 1,
				name = "Folder 9",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 2,
				name = "Folder 8",
				parentId = 1,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 3,
				name = "Album 9",
				parentId = 1,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 4,
				name = "Album 8",
				parentId = 2,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		}
		
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
		-- create a folder in an album (failure)
		rxTest:expectOnlyError( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','createFolder', wfCatalog, 'Folder 10', 3 ), "InvalidParent" )
		
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
		-- move a folder into an album (failure)
		rxTest:expectOnlyError( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentAlbumOrFolder', wfCatalog, 4, 3 ), "InvalidParent" )
		
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentAlbumOrFolder', wfCatalog, 3, 2 ) )
		
		-- move an album into a different folder (success)

		foldersWithNestedAlbums[ 3 ].sortOrder = 'V--0'
		foldersWithNestedAlbums[ 4 ].sortOrder = 'V'
		
		foldersWithNestedAlbums[ 3 ].parentId = 2
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentMultipleAlbumsOrFolders', wfCatalog, { 3, 4 }, 1 ), nil )
		
		foldersWithNestedAlbums[ 3 ].parentId = 1
		foldersWithNestedAlbums[ 4 ].parentId = 1
		
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','reparentMultipleAlbumsOrFolders', wfCatalog, { 3, 4 }, { [3] = 2, [4] = nil } ), nil )
		
		foldersWithNestedAlbums[ 3 ].parentId = 2
		foldersWithNestedAlbums[ 4 ].parentId = nil
		
		folderHierarchy:expectDataWithinTime( 3, foldersWithNestedAlbums )
		
	end

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

	function WFCatAlbumsTest.rxTestAlbumImportSourceAndDocId( rxTest )
		
		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local addFolderFn = makeAdderOfFolder( rxTest, wfCatalog )
		
		local importSource1 = {
			lrcatStoreProviderId = "lrcatStoreProviderId",
			lrcatAlbumId = "local_id",
		}
		
		local importSource2 = { dummy = true }
		
		local importSource3 = { 'PSE Catalog' }

		local guid1 = KSIdGenerator.uuidInLowercaseHex()
		local guid2 = KSIdGenerator.uuidInLowercaseHex()
		local guid3 = KSIdGenerator.uuidInLowercaseHex()

		addAlbumFn( nil, nil, importSource1, guid1 )
		addFolderFn( nil, importSource2, guid2 )
		addAlbumFn( 2, nil, importSource3, guid3 )

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:flatMap( function( albums ) 
				
				return Observable.from( albums )
					:flatMap( function( album )
						-- Get the doc value so we can compare the importSource
						return wfCatalog:docStore():docValue( album.fullDocId )
					end )
					:map( function( albumDoc )
						-- Pluck out only the localId and importSource
						return {
							id = albumDoc._localId,
							importSource = albumDoc.importSource,
							guid = albumDoc._id,
						}
					end )
					:toArray()
			end ) )

		local expectedAlbumsData = {
			{
				id = 1,
				guid = guid1,
				importSource = importSource1,
			},
			{
				id = 2,
				guid = guid2,
				importSource = importSource2,
			},
			{
				id = 3,
				guid = guid3,
				importSource = importSource3,
			},
		}

		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

	end

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

	function WFCatAlbumsTest.rxTestDeleteAlbumsAndFolders( rxTest )
		
		rxTest:setTimeLimit( 60 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )
		
		local addFolderFn = makeAdderOfFolder( rxTest, wfCatalog )
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		addFolderFn() -- 1
		addAlbumFn() -- 2
		
		local albumHierarchy = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )
		
		albumHierarchy:expectDataWithinTime( 3, {
				{
					id = 1,
					name = "Folder 9",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				},
				{
					id = 2,
					name = "Album 9",
					subtype = "collection",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				},
			} )
		
		-- delete folder
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 1 ) )
		
		albumHierarchy:expectDataWithinTime( 3, {
				{
					id = 2,
					name = "Album 9",
					subtype = "collection",
					hidden = false,
					hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				},
			} )
		
		-- delete album
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 2 ) )
		
		albumHierarchy:expectDataWithinTime( 3, { } )
		
		-- create nested structure
		addFolderFn() -- 3
		addFolderFn() -- 4
		addFolderFn( 4 ) -- 4 -> 5
		addAlbumFn( 3 ) -- 3 -> 6
		addAlbumFn( 5 ) -- 4 -> 5 -> 7
		
		albumHierarchy:expectDataWithinTime( 3, {
			{
				id = 3,
				name = "Folder 8",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 4,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 5,
				name = "Folder 6",
				parentId = 4,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 6,
				name = "Album 8",
				parentId = 3,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 7,
				name = "Album 7",
				parentId = 5,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			} )
		
		-- delete nested album (should leave containing folder)
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 6 ) )
		
		albumHierarchy:expectDataWithinTime( 3, {
			{
				id = 3,
				name = "Folder 8",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 4,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 5,
				name = "Folder 6",
				parentId = 4,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 7,
				name = "Album 7",
				parentId = 5,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			} )
		
		-- delete top level album (children should go too)
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 4 ) )
		
		albumHierarchy:expectDataWithinTime( 3, {
				{
					id = 3,
					name = "Folder 8",
					subtype = "collection_set",
					hidden = false,
						hasDisplayFilters = 0,
					fullDocId = "uuid",
					createdTimestamp = "recently",
					updateTimestamp = "recently",
				}
			} )
		
		-- SPAM [matthewj 2017-01-20]: do we need to check that all assets for an album are also deleted?
	end

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

	function WFCatAlbumsTest.rxTestBasicFolders( rxTest )
		
		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local addFolderFn = makeAdderOfFolder( rxTest, wfCatalog )

		addFolderFn()
		addFolderFn()
		addFolderFn()

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		local expectedAlbumsData = {
			{
				id = 1,
				name = "Folder 9",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 2,
				name = "Folder 8",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 3,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		}

		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','renameAlbumOrFolder', wfCatalog, 3, "Renamed Album" )
		)
		expectedAlbumsData[ 3 ].name = 'Renamed Album'
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 1 )
		)
		table.remove( expectedAlbumsData, 1 )
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 2 )
		)
		
		
	end

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

	function WFCatAlbumsTest.rxTestBasicAlbums( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		addAlbumFn()
		addAlbumFn()
		addAlbumFn( nil, true ) -- create hidden Album

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		local expectedAlbumsData = {
			{
				id = 1,
				name = "Album 9",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 2,
				name = "Album 8",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		}

		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','renameAlbumOrFolder', wfCatalog, 2, "Renamed Album" )
		)
		expectedAlbumsData[ 2 ].name = 'Renamed Album'
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 1 )
		)
		table.remove( expectedAlbumsData, 1 )
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 2 )
		)

	end

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

	function WFCatAlbumsTest.rxTestBasicSmartAlbums( rxTest )

		rxTest:setTimeLimit( 60 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )
		
		local addSmartAlbumFn = makeAdderOfSmartAlbum( rxTest, wfCatalog )
		
		local predicates1 = {
			class = "and",
			terms = {
				class = "keyword",
				terms = {
					op = "is",
					value = "test"
				}
			}
		}
		
		local predicates2 = {
			class = "and",
			terms = {
				class = "rating",
				terms = {
					op = "is_greater_equal",
					value = 3
				}
			}
		}
		
		addSmartAlbumFn( nil, predicates1 )
		addSmartAlbumFn( nil, predicates2 )
		
		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		local expectedAlbumsData = {
			{
				id = 1,
				name = "Smart Album 9",
				subtype = "smart",
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
				predicates = predicates1
			},
			{
				id = 2,
				name = "Smart Album 8",
				subtype = "smart",
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
				predicates = predicates2
			},
		}

		albumsData:expectDataWithinTime( 10, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','renameAlbumOrFolder', wfCatalog, 2, "Renamed Smart Album" )
		)
		expectedAlbumsData[ 2 ].name = 'Renamed Smart Album'
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 1 )
		)
		table.remove( expectedAlbumsData, 1 )
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','deleteAlbumOrFolder', wfCatalog, 2 )
		)
		
	end
	
	----------------------------------------------------------------------------
	
	function WFCatAlbumsTest.rxTestUpdateSmartAlbums( rxTest )
		
		rxTest:setTimeLimit( 60 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )
		
		local addSmartAlbumFn = makeAdderOfSmartAlbum( rxTest, wfCatalog )
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		local addFolderFn = makeAdderOfFolder( rxTest, wfCatalog )
		
		local predicates1 = {
			class = "and",
			terms = {
				class = "keyword",
				terms = {
					op = "is",
					value = "test"
				}
			}
		}
		
		-- create a folder
		addFolderFn()								-- adds folder with id=1
		addSmartAlbumFn( nil, predicates1 )			-- adds smart album with id=2
		addAlbumFn( 1 )								-- adds album with id=3, parentId=1
		
		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )
		
		local expectedAlbumsData = {
			{
				createdTimestamp = "recently",
				fullDocId = "uuid",
				hasDisplayFilters = 0,
				hidden = false,
				id = 1,
				name = "Folder 9",
				subtype = "collection_set",
				updateTimestamp = "recently",
			},
			{
				createdTimestamp = "recently",
				fullDocId = "uuid",
				id = 2,
				name = "Smart Album 9",
				predicates = {
					class = "and",
					terms = {
						class = "keyword",
						terms = {
							op = "is",
							value = "test",
						},
					},
				},
				subtype = "smart",
				updateTimestamp = "recently",
			},
			{
				createdTimestamp = "recently",
				fullDocId = "uuid",
				hasDisplayFilters = 0,
				hidden = false,
				id = 3,
				name = "Album 9",
				parentId = 1,
				subtype = "collection",
				updateTimestamp = "recently",
			},
			
		}
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
	
		rxTest:waitFor(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','updateSmartAlbum', wfCatalog, 2, "Renamed Smart Album", 1, predicates1 )
		)
		expectedAlbumsData[ 2 ].name = 'Renamed Smart Album'
		expectedAlbumsData[ 2 ].parentId = 1
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		
		
	end
	
	----------------------------------------------------------------------------

	function WFCatAlbumsTest.rxTestHiddenAlbum( rxTest )

		rxTest:setTimeLimit( 10 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		local albumId = addAlbumFn( nil, true ) -- create hidden Album

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		albumsData:expectDataWithinTime( 5, {} )
		
		local localAlbumId
		rxTest:expectSingleValue(
			wfCatalog:docStore()
				:docIdToLocalId( albumId )
				:tap( function( localId )
					localAlbumId = localId
				end )
		, ':ignore:' )	
		
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','unhideAlbum', wfCatalog, localAlbumId )
		)
		
		albumsData:expectDataWithinTime( 5, {
			{
				id = 1,
				name = "Album 9",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		} )

	end

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

	function WFCatAlbumsTest.rxTestAlbumTitle( rxTest )

		rxTest:setTimeLimit( 10 )
		
		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		local albumId = addAlbumFn()

		local albumTitleObservable = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:pluck( 1 )
			:pluck( 'title' )
		)

		albumTitleObservable:expectDataWithinTime( 5, nil )
		
		rxTest:expectSingleValue(
			wfCatalog:docStore()
				:applyOverlay( {
					_id = albumId,
					story = {
						title = 'my album title',
					},
				} ), ":ignore:"
		)
		
		albumTitleObservable:expectDataWithinTime( 5, 'my album title' )

	end

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

	function WFCatAlbumsTest.rxTestCover( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )
		local docStore = wfCatalog:docStore()
		
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )
		
		local addAssetFn =  WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, date)
				-- set date to what we want
				baseDocValue.captureDate = date
				return baseDocValue
			end )
			
		local albumAssetAdderFn = WFOzTestUtils.v2.makeAdderOfAssetToAlbum( rxTest, docStore )
		
		local albumId = addAlbumFn()
		local asset1 = addAssetFn( '2001' )
		local asset2 = addAssetFn( '2002' )
		local asset3 = addAssetFn( '2003' )

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','albumsWithSyncStatus', wfCatalog )
												:map( sanitizeAndSortAlbumsList )
												:map( function( albums )
												
													for _, album in pairs( albums ) do
														album.count = nil
													end
												
													return albums
												
												end ) )
		
		local coverImageData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','albumCover', wfCatalog, albumId ):map( function( coverId ) return coverId end ) )

		local expectedAlbumsData = {			
			{
				id = 1,
				name = "Album 9",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		}

		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, nil )
		
		-- add explicit cover with local id
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, 1, 2 )
		)
		
		expectedAlbumsData[ 1 ].cover = 2
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, 2 )
		
		-- change explicit cover with local id
		rxTest:expectOnlyCompleted( 
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, 1, 3 ) 
		)
		
		expectedAlbumsData[ 1 ].cover = 3
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, 3 )
		
		-- remove explicit cover
		rxTest:expectOnlyCompleted( 
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, 1, nil ) 
		)
		
		expectedAlbumsData[ 1 ].cover = nil
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, nil )
		
		-- add cover with global ids
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, albumId, asset1 )
		)
		
		expectedAlbumsData[ 1 ].cover = 2
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, 2 )
		
		-- change cover with global ids
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, albumId, asset2 )
		)
		
		expectedAlbumsData[ 1 ].cover = 3
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, 3 )
		
		-- remove cover with global ids
		rxTest:expectOnlyCompleted( 
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, albumId, nil ) 
		)
		
		expectedAlbumsData[ 1 ].cover = nil
		
		albumsData:expectDataWithinTime( 10, expectedAlbumsData )
		coverImageData:expectDataWithinTime( 3, nil )
		
		
		coverImageData:expectDataWithinTime( 5, nil )
		
		-- implicit covers should be ordered by image capture date ascending so adding 
		-- older assets should change the implicit cover
		albumAssetAdderFn( albumId, asset3 )
		coverImageData:expectDataWithinTime( 15, 4 )
		
		albumAssetAdderFn( albumId, asset2 )
		coverImageData:expectDataWithinTime( 15, 3 )
		
		albumAssetAdderFn( albumId, asset1 )
		coverImageData:expectDataWithinTime( 15, 2 )
		
		-- change cover to an explicit id, and then remove it and verify the cover
		-- changes.

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAlbumCover', wfCatalog, albumId, asset2 )
		)

		coverImageData:expectDataWithinTime( 15, 3 )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','removeAssetsFromAlbum', wfCatalog, albumId, { asset2 } )
		)

		coverImageData:expectDataWithinTime( 15, 2 )

	end

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

	function WFCatAlbumsTest.DISABLED_rxTestLots_testPerformance( rxTest )

		rxTest:setTimeLimit( 3600 )

		local sf = string.format
		local mod = math.mod
		local floor = math.floor
		local sqrt = math.sqrt

		for o = 1, 15 do
			local wfCatalog = createFakeWFCatalog( rxTest )

			local nextBatch = {}
			local function sendBatch()
				local batch = nextBatch
				nextBatch = {}
				rxTest:expectOnlyCompleted( wfCatalog:docStore():bulkSetDocValue( batch )
					:takeLast( 0 ) )
			end
			local addAlbumFn = function( n )
				local parentId = n > 10 and ( mod( n, floor( sqrt( n ) ) ) + 0 ) or nil
				local name
				if parentId then
					name = sf( 'Album %05d / Album %05d', parentId, n )
				else
					name = sf( 'Album %05d', n )
				end

				nextBatch[ #nextBatch + 1 ] = {
					type = 'album',
					subtype = 'collection',
					_id = sf( 'abcdabcdabcdabcd%016x', n ),
					name = name,
					parent = n > 10 and {
						id = sf( 'abcdabcdabcdabcd%016x', parentId ),
					} or nil,
					_serverProvidedMetadata = {
						updated = '2001',
					},
					_localOnly = {
						updateTimeStamp = '2001',
					},
					order = sf( '%07x', 1000000 - n ),
				}
				if #nextBatch >= 100 then
					sendBatch()
				end
			end

			local N = math.pow( 2, o )
			for i = 1, N do
				addAlbumFn( i )
			end
			sendBatch()

			for i = 1, 3 do

				rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
					:map( function( albums )
							for _, album in ipairs( albums ) do
								assert( next( album ), 'all albums have at least something in them' )
							end

							return #albums == N
						end ) )
					:expectDataWithinTime( 400, true )
			end

		end

	end

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

	local function fillToXChars( name, fillTo )

		return name .. string.rep( '.', math.max( 0, fillTo - string.len( name ) ) )

	end

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

	function WFCatAlbumsTest.rxTestSimpleAlbumSync_testNightly( rxTest )
		
		rxTest:setTimeLimit( 50 )
		
		local session = WFCatTestSession( rxTest, 'NmbCatalog' )
		
		local catalog

		rxTest:expectSingleValue(

			session:catalog()
				:tap( function( _catalog )
					check( 'WFCatalog' )
					catalog = _catalog
				end ),

			':ignore:' )
		
		local continuousTaskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 1,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		}

		local options = {
			continuousTaskDelays = continuousTaskDelays,
			isCatalogPuller = true
		}
		
		local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
		
		local pusher = WFCatMetadataPusher( catalog, options )
		
		catalog:addCloseAction( function()
			puller:stopContinuousUpdates()
			pusher:stopContinuousUpdates()
			puller:close()
			pusher:close()
		end )
		
		puller:getUpdatesContinuously()
		rxTest:waitFor( pusher:sendUpdatesContinuously() )
		local albumName = "Actual Album"
		
		local albumLocalId1, albumLocalId2, albumLocalId3
		local albumFullDocId1, albumFullDocId2, albumFullDocId3
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','createAlbum', catalog, albumName )
			:tap( function( localAlbumId, globalAlbumId )
				albumLocalId1 = localAlbumId
				albumFullDocId1 = globalAlbumId
			end )
			:takeLast( 0 )
		)
		
		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','albumsWithSyncStatus', catalog )
			:map( function( albums )
				-- This is a sync-test. We don't want the albums sanitizer to mask it out
				return albums, true
			end )
			:map( sanitizeAndSortAlbumsList ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = albumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		} )
		
		local longAlbumName = fillToXChars( [[THIS NAME SHOULD BE EXACTLY 250 CHARS]], 250 )
		
		local tooLongAlbumName = longAlbumName .. [[THIS SHOULD BE TRUNCATED]]
		
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','createAlbum', catalog, tooLongAlbumName )
			:tap( function( localAlbumId, globalAlbumId )
				albumLocalId2 = localAlbumId
				albumFullDocId2 = globalAlbumId
			end )
			:takeLast( 0 )
		)

		albumsData:expectDataWithinTime( 25, 
		{	
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = albumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,			
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		} )
		
		local longAlbumName2 = string.replace( longAlbumName, 'NAME', 'ALSO' )
		local tooLongAlbumName2 = longAlbumName2 .. [[THIS SHOULD BE TRUNCATED]]
		
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','renameAlbumOrFolder', catalog, albumLocalId1, tooLongAlbumName2 ):takeLast( 0 ) )
		
		albumsData:expectDataWithinTime( 25, 
		{	
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = longAlbumName2,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		} )

		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','createAlbum', catalog, albumName, nil, true )
			:tap( function( localAlbumId, globalAlbumId )
				albumLocalId3 = localAlbumId
				albumFullDocId3 = globalAlbumId
			end )
			:takeLast( 0 )
		)
		
		-- recreating the observable to include the hidden attribute
		albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','albumsWithSyncStatus', catalog )
			:map( function( albums )
				-- This is a sync-test. We don't want the albums sanitizer to mask it out
				return albums, true
			end )
			:map( sanitizeAndSortAlbumsList ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = longAlbumName2,
				syncStatus = "synced",
				subtype = "collection",
				hidden = false,
				count = 0,
				hasDisplayFilters = 0,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				hidden = false,
				count = 0,
				hasDisplayFilters = 0,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			}
		} )
	end

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

	function WFCatAlbumsTest.rxTestGetAlbumMarkedForOfflineStatus( rxTest )

		rxTest:setTimeLimit( 30 )

		local wfCatalog = createFakeWFCatalog( rxTest )
		local docStore = wfCatalog:docStore()
		
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		local albumId1 = addAlbumFn()
		local albumId2 = addAlbumFn()
		local albumId3 = addAlbumFn()

		rxTest:expectSingleValue( 
			docStore:applyOverlay {
						_id = albumId2,
						_localOnly = {
							isMarkedForOfflineUse = true
						}
					},
			':ignore:' )

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumMarkedForOfflineStatus', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		local expectedAlbumsData = {
			[albumId1] = false,
			[albumId2] = true,
			[albumId3] = false,
		}

		albumsData:expectDataWithinTime( 5, expectedAlbumsData )

		rxTest:expectSingleValue( 
			docStore:applyOverlay {
						_id = albumId3,
						_localOnly = {
							isMarkedForOfflineUse = true
						}
					},
			':ignore:' )

		expectedAlbumsData = {
			[albumId1] = false,
			[albumId2] = true,
			[albumId3] = true,
		}

		albumsData:expectDataWithinTime( 5, expectedAlbumsData )

	end

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

	function WFCatAlbumsTest.rxTestGetSizeOfAlbumsForLocalUse( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )
		local docStore = wfCatalog:docStore()
		
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		local albumId1 = addAlbumFn()
		local albumId2 = addAlbumFn()
		
		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), 0 )
					
		local optArguments = {
			hasMasks = true,
			noLocalMasks = true,
			noLocalOriginal = true,
			noLocalProxy = true
		}
		
		local asset1, expectedData = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )
		local asset2 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )
		local asset3 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )
		local asset4 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )

		rxTest:waitFor( docStore:bulkSetDocValue( { asset1, asset2, asset3, asset4 } ) )

		local assetId1 = asset1._id
		local assetId2 = asset2._id
		local assetId3 = asset3._id
		local assetId4 = asset4._id

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId1, { assetId1, assetId2 } )
		)

		local expectedSize = expectedData.originalFileSize
				+ expectedData.mask1FileSize
				+ expectedData.mask2FileSize
				+ WFCatAssetUtils.estimateProxySize( asset1 )

		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), expectedSize * 2 )

		-- Add Assets to Second Album

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId2, { assetId3, assetId4  } )
		)

		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), expectedSize * 4 )

	end

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

	function WFCatAlbumsTest.rxTestSetAssetSortOrder( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )

		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		addAlbumFn()
		addAlbumFn()
		addAlbumFn( nil, true ) -- create hidden Album

		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getAlbumsList', wfCatalog )
			:map( sanitizeAndSortAlbumsList ) )

		local expectedAlbumsData = {
			{
				id = 1,
				name = "Album 9",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
			{
				id = 2,
				name = "Album 8",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
				fullDocId = "uuid",
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		}

		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAssetSortOrder', wfCatalog, 2, "ratingAsc" )
		)
		expectedAlbumsData[ 2 ].assetSortOrder = 'ratingAsc'
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAssetSortOrder', wfCatalog, 2, "userUpdatedDesc" )
		)
		expectedAlbumsData[ 2 ].assetSortOrder = 'userUpdatedDesc'
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		-- Test unset case with nil value
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAssetSortOrder', wfCatalog, 2, nil )
		)
		expectedAlbumsData[ 2 ].assetSortOrder = nil
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		-- Test unset case with "" value
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAssetSortOrder', wfCatalog, 1, "ratingAsc" )
		)
		expectedAlbumsData[ 1 ].assetSortOrder = "ratingAsc"
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )

		-- unset
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','setAssetSortOrder', wfCatalog, 1, "" )
		)
		expectedAlbumsData[ 1 ].assetSortOrder = nil
		
		albumsData:expectDataWithinTime( 40, expectedAlbumsData )
		
	end

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

	function WFCatAlbumsTest.rxTestAddAssetsToAlbumWithOrder( rxTest )

		rxTest:setTimeLimit( 60 )

		local wfCatalog = createFakeWFCatalog( rxTest )
		local docStore = wfCatalog:docStore()
		local indexStore = wfCatalog:indexStore()
		
		local addAlbumFn = makeAdderOfAlbum( rxTest, wfCatalog )

		local albumId1 = addAlbumFn()
					
		local optArguments = {
			hasMasks = true,
			noLocalMasks = true,
			noLocalOriginal = true,
			noLocalProxy = true
		}
		
		local asset1 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )
		local asset2 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optArguments )

		rxTest:waitFor( docStore:bulkSetDocValue( { asset1, asset2 } ) )

		local assetId1 = asset1._id
		local assetId2 = asset2._id

		local assetIds = { assetId1, assetId2 }
		local sortOrders = {"---1" , "---2"}
		
		rxTest:waitFor(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbumWithOrder', wfCatalog, albumId1, assetIds , sortOrders )
		)

		rxTest:expectSingleValue(
			indexStore:queryOnce {
				sql = [[ SELECT sortOrder FROM album_asset_v2 
						 WHERE albumId = ? AND assetId = ? ]],
				quiesceUntilUpToDate = true,
				albumId1,
				assetId2,
			} 
			:map( function( rows )
				return rows[ 1 ]
			end)
			
		, { "---2" } )
	end
	
	----------------------------------------------------------------------------

	function WFCatAlbumsTest.rxTestAlbumNameLength_testNightly( rxTest )
		
		rxTest:setTimeLimit( 50 )
		
		local session = WFCatTestSession( rxTest, 'NmbCatalog' )
		
		local catalog

		rxTest:expectSingleValue(

			session:catalog()
				:tap( function( _catalog )
					check( 'WFCatalog' )
					catalog = _catalog
				end ),

			':ignore:' )
		
		local continuousTaskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 1,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		}

		local options = {
			continuousTaskDelays = continuousTaskDelays,
			isCatalogPuller = true
		}
		
		local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
		
		local pusher = WFCatMetadataPusher( catalog, options )
		
		catalog:addCloseAction( function()
			puller:stopContinuousUpdates()
			pusher:stopContinuousUpdates()
			puller:close()
			pusher:close()
		end )
		
		puller:getUpdatesContinuously()
		rxTest:waitFor( pusher:sendUpdatesContinuously() )
		
		-- Create an album name that is over the limit.
		
		local OZ_MAX_ALBUM_NAME_CHARS = 250
		
		local japaneseseChar = '\229\137\141'
		
		local expectedAlbumName = ""
		
		for i = 1, OZ_MAX_ALBUM_NAME_CHARS, 1 do
			expectedAlbumName = string.format( "%s%s", expectedAlbumName, japaneseseChar )
		end
		
		local tooLongAlbumName = expectedAlbumName .. expectedAlbumName

		local albumLocalId, albumFullDocId
		
		rxTest:expectOnlyCompleted( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','createAlbum', catalog, tooLongAlbumName )
			:tap( function( localAlbumId, globalAlbumId )
				albumLocalId = localAlbumId
				albumFullDocId = globalAlbumId
			end )
			:takeLast( 0 )
		)
		
		local albumsData = rxTest:observe( callFunctionOrSubmodel(
					rxTest, catalog,
					'WFCatAlbums','albumsWithSyncStatus', catalog )
			:map( function( albums )
				-- This is a sync-test. We don't want the albums sanitizer to mask it out
				return albums, true
			end )
			:map( sanitizeAndSortAlbumsList ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId,
				fullDocId = albumFullDocId,
				name = expectedAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
				hidden = false,
				createdTimestamp = "recently",
				updateTimestamp = "recently",
			},
		} )
		
	end

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

	return maybeExistingTestClass and WFCatAlbumsTest
			or WFRxTestCase.wrapSuite( 'WFCatAlbumsTest', WFCatAlbumsTest )
	
end 
