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

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.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local _ = require 'WFArguments_uuid'

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

	-- WF.oz_client.managed_catalog
local WFCatMockCatalog = require 'WFCatMockCatalog'
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 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 )
		
		for _, album in ipairs( albums or {} ) do
			album.fullDocId = nil
			album.syncStatus = nil
		end
		
		table.sort( albums, function( a, b )
				return a.id < b.id
			end )
		return albums
	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.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,
				} } )
		
		-- create a folder in a folder
		addFolderFn( 1 )
		
		local folderWithNestedFolder =  {
				{
					id = 1,
					name = "Folder 9",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
				},
				{
					id = 2,
					parentId = 1,
					name = "Folder 8",
					subtype = "collection_set",
					hidden = false,
					hasDisplayFilters = 0,
				},
			}
			
		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,
			},
			{
				id = 2,
				name = "Folder 8",
				parentId = 1,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 3,
				name = "Album 9",
				parentId = 1,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 4,
				name = "Album 8",
				parentId = 2,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
		}
		
		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,
				},
				{
					id = 2,
					name = "Album 9",
					subtype = "collection",
					hidden = false,
					hasDisplayFilters = 0,
				},
			} )
		
		-- 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,
				},
			} )
		
		-- 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,
			},
			{
				id = 4,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 5,
				name = "Folder 6",
				parentId = 4,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 6,
				name = "Album 8",
				parentId = 3,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 7,
				name = "Album 7",
				parentId = 5,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
			} )
		
		-- 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,
			},
			{
				id = 4,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 5,
				name = "Folder 6",
				parentId = 4,
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 7,
				name = "Album 7",
				parentId = 5,
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
			} )
		
		-- 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,
				}
			} )
		
		-- 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,
			},
			{
				id = 2,
				name = "Folder 8",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
			{
				id = 3,
				name = "Folder 7",
				subtype = "collection_set",
				hidden = false,
				hasDisplayFilters = 0,
			},
		}

		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,
			},
			{
				id = 2,
				name = "Album 8",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
		}

		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.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,
			},
		} )

	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,
			},
		}

		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()
		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( results )
				for _, album in ipairs( results ) do
					album.updateTimestamp = nil
					album.sortOrder = nil
					album.hidden = nil
				end
				return results
			end ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = albumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
			},
		} )
		
		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,			
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
			},
		} )
		
		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,
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
			},
		} )

		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( results )
				for _, album in ipairs( results ) do
					album.updateTimestamp = nil
					album.sortOrder = nil
				end
				return results
			end ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId1,
				fullDocId = albumFullDocId1,
				name = longAlbumName2,
				syncStatus = "synced",
				subtype = "collection",
				hidden = false,
				count = 0,
				hasDisplayFilters = 0,
			},
			{
				id = albumLocalId2,
				fullDocId = albumFullDocId2,
				name = longAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				hidden = false,
				count = 0,
				hasDisplayFilters = 0,
			}
		} )
	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 addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore,
			function( baseDocValue, originalSize, proxySize, willNotUploadMaster )
			
				baseDocValue._localOnly = {}
			
				if originalSize then
				
					baseDocValue.importSource = {
						fileSize = originalSize
					}

					baseDocValue._serverProvidedMetadata = {
						links = {
							[ '/rels/master' ] = {
								href = "foo/foo/foo",
							},
						}
					}
					
				end
				
				if proxySize then

					baseDocValue._localOnly = {
						files = {
							proxy = {
								[ '2560' ] = { size = proxySize }
							}
						}
					}
				end

				if willNotUploadMaster then

					if baseDocValue.importSource then
						baseDocValue.importSource.willNotUploadMaster = true
					end

				end

				return baseDocValue
				
			end )

		addAlbumFn()
		
		local assetId1 = addAssetFn( 1000 )
		local assetId2 = addAssetFn( nil, 50 )
		local assetId3 = addAssetFn( 2000, 30 )
		local assetId4 = addAssetFn( 0, 0 )
		
		rxTest:expectOnlyCompleted(
			callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','addAssetsToAlbum', wfCatalog, albumId1, { assetId1, assetId2, assetId3, assetId4 } )
		)

		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), 3080 )

		local assetId5 = addAssetFn( 101 )

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

		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), 3181 )

		-- Don't count original size of assets marked with willNotUploadMaster as they don't have a master.

		local assetId6 = addAssetFn( 5000, 500, true )

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

		rxTest:expectSingleValue( callFunctionOrSubmodel(
					rxTest, wfCatalog,
					'WFCatAlbums','getSizeOfAlbumsForLocalUse', wfCatalog, { albumId1, albumId2 } ), 3681 )

	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,
			},
			{
				id = 2,
				name = "Album 8",
				subtype = "collection",
				hidden = false,
				hasDisplayFilters = 0,
			},
		}

		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.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()
		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( results )
				for _, album in ipairs( results ) do
					album.updateTimestamp = nil
					album.sortOrder = nil
					album.hidden = nil
				end
				return results
			end ) )
		
		albumsData:expectDataWithinTime( 25, {
			{
				id = albumLocalId,
				fullDocId = albumFullDocId,
				name = expectedAlbumName,
				syncStatus = "synced",
				subtype = "collection",
				count = 0,
				hasDisplayFilters = 0,
			},
		} )
		
	end

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

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