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

WFCatSyncStatusTest.lua

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

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

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

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

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

local WFCatTestSession = require 'WFCatTestSession'

	-- WF.oz_client.managed_catalog
local WFCatAlbums = require 'WFCatAlbums'
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'

	-- WF.oz_client.managed_catalog.sync
local WFCatSyncStatus = require 'WFCatSyncStatus'
local WFOzTestUtils = require 'WFOzTestUtils'

	-- lightroom cc
local WFCatVolumeAgent = require 'WFCatVolumeAgent'

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

local check = assert( WFArguments.check )

local createFakeWFCatalog = WFCatMockCatalog.createFakeWFCatalog

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

local options = {

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

	isCatalogPuller = true

}

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

-- @mark WFCatSyncStatusTest

local WFCatSyncStatusTest = {}

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAssets( rxTest )

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

	local function updateRevIdToMimicOzAcceptedRevision( rxTest, docStore, assetId1 )

		-- Get latest local revId for asset (as we modified it to set importSource.status)

		local assetRevId2 = rxTest:getFirstValue(
				docStore:docValue( assetId1 )
					:map( function( docValue )
						return docValue._revId
					end ) )

		-- Make it look like Oz accepted our latest revision.

		rxTest:waitFor(
			docStore:applyOverlay {
				_id = assetId1,
				_serverProvidedMetadata = {
					revision_ids = {
						assetRevId2
					}
				}
			} )

	end

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

	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local docStore = catalog:docStore()

	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore, function( docValue )
			return docValue
		end )

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( catalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					nonZeroStatuses[ k ] = v
				end
			end
			return nonZeroStatuses
		end )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{ -- no assets with any status
			} )

	local syncPendingObservable = WFCatSyncStatus.syncPendingForAnyAsset( catalog )
	local persistentSyncErrorObservable = WFCatSyncStatus.persistentSyncErrorForAnyAsset( catalog )
	local getLocalIdsWithSyncPendingOrErrorObservable = WFCatSyncStatus.getLocalIdsWithSyncPendingOrError( catalog )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( persistentSyncErrorObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, {} )

	local assetId1, assetRevId1 = addAssetFn()
	local assetLocalId1 = 1

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsMetadataUploaded = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Make it look the asset can't be uploaded because we ran out of storage on oz.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				ozError = {
					subtype = 'StorageExceededError',
				},
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				StorageLimitExceeded = 1,
			} )

	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Remove the error field.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				ozError = { _delete = true },
			},
		} )

	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- make it look like we received the same from oz and processed
	-- with WFCatMetadataPuller, but still need to upload original
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = {
						master = {
							relativePath = 'some/path',
							volumeGuid = '<guid>'
						},
					}
				}
			},
			_serverProvidedMetadata = {
				links = {
				},
				revision_ids = {
					assetRevId1
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsOriginalUploaded = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Make it looks like we can't upload the original because the storage
	-- has been exceeded.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				ozError = {
					subtype = 'StorageExceededError',
				},
			},
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				StorageLimitExceeded = 1,
			} )

	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Remove the error field.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				ozError = { _delete = true },
			},
		} )

	-- Make it look like oz received the original.
	-- Make our copy of the original disappear.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			reviews = {
				[ 'foo' ] = {
					flag = 'pick'
				},
			},
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { href = "something" },
					["/rels/proxy_type/2560"] = { href = "something" }
				},
			}
		} )

	updateRevIdToMimicOzAcceptedRevision( rxTest, docStore, assetId1 )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsProxyDownloaded = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Make it look like we have the original, no need for a proxy.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = {
						master = {
							relativePath = 'foo',
							volumeGuid = 'bar'
						},
					}
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				Completed = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, {} )

	-- Make it look like the original received by oz was corrupted but
	-- the local copy is still valid.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			importSource = {
				status = {
					error = {
						all = {
							reason = {
								sha256 = 'does not match'
							}
						}
					}
				}
			},
		} )

	updateRevIdToMimicOzAcceptedRevision( rxTest, docStore, assetId1 )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsOriginalUploaded = 1,
			} )

	-- Make it look like the original received by oz was corrupted and
	-- local copy is corrupted as well (detected by the file's sha256
	-- hash not matching the hash computed in the importSource earlier).

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				localOriginalError = 'InvalidLocalOriginal',
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				InvalidLocalOriginal = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( persistentSyncErrorObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- Remove the error fields and update revision id.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				localOriginalError = { _delete = true },
			},
			importSource = {
				status = { _delete = true },
			},
		} )

	updateRevIdToMimicOzAcceptedRevision( rxTest, docStore, assetId1 )

	-- Remove proxy from Oz and remove local original. We should now think we need the original.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/proxy_type/2560"] = { _delete = true }
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsOriginalDownloaded = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- make it look like we received the same from oz and processed
	-- with WFCatMetadataPuller and the asset is complete

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = {
						master = {
							relativePath = 'foo',
							volumeGuid = 'bar'
						},
					}
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { href = "something" }
				},
				wfOzPayloadHash = '67166b4c3ed12f8b93957333a6951a66c0aed286'
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				Completed = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, {} )

	-- Make it look like we sync has completed without master but because the asset
	-- was uploaded by Lightroom.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = { _delete = true },
					proxy = {
						['2560'] = {
							relativePath = 'foo',
							volumeGuid = 'bar'
						},
				}
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { _delete = true },
					["/rels/proxy_type/2560"] = { href = "something" }
				},
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				CompletedNoMaster = 1,
			} )

	-- Simulate a non-displayable asset. This should not have a sync status.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = { _delete = true },
					proxy = { _delete = true }
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { _delete = true },
					["/rels/proxy_type/2560"] = { _delete = true },
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{} )
			
	-- Simulate a fully synced asset from the space-feed with no master or proxy
	-- on Oz.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_serverProvidedMetadata = {
				links = {
					["/rels/rendition_type/thumbnail2x"] = {
						href = 'foo'
					}
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				CompletedNoNegatives = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, {} )

	-- No proxy, no original, but marked for offline use

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = { _delete = true },
				isMarkedForOfflineUse = true
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { href = "something" },
					["/rels/proxy_type/2560"] = { href = "something" },
					["/rels/rendition_type/thumbnail2x"] = { _delete = true }
				},

			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsOriginalDownloadedForOffline = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- proxy, no original, marked for offline use

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					proxy = {
						['2560'] = {
							relativePath = 'foo',
							volumeGuid = 'bar'
						},
					}
				}
			},
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsOriginalDownloadedForOffline = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

	-- proxy, original, marked for offline use

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = {
					original = {
						master = {
							relativePath = 'foo',
							volumeGuid = 'bar'
						},
					}
				}
			},
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				CompletedForOffline = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, false )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, {} )

	-- proxy download error
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = { _delete = true },
				isMarkedForOfflineUse = false,
				binaryDownloadErrors = {
					['/rels/proxy_type/2560'] = {
						['something'] = true
					}
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/proxy_type/2560"] = { href = "something" },
					["/rels/master"] = { _delete = true }
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				ProxyDownloadError = 1,
			} )

	-- original download error
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				files = { _delete = true },
				binaryDownloadErrors = {
					['/rels/proxy_type/2560'] = { _delete = true },
					['/rels/master'] = {
						['something'] = true
					}
				}
			},
			_serverProvidedMetadata = {
				links = {
					["/rels/proxy_type/2560"] = { _delete = true },
					["/rels/master"] = { href = "something" },
				}
			}
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				OriginalDownloadError = 1,
			} )

	-- deleted but oz not notified of deletion yet.

	-- Cloud trash only considers deleteTrash as assets
	-- that need to be synced. Regular deletes are only
	-- counted by the trash asset that is created.
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_deleted = true,
			_localOnly = { universal = { trash = { isDeleteTrash = true } } },
		} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsMetadataUploaded = 1,
			} )

	rxTest:observe( syncPendingObservable ):expectDataWithinTime( 5, true )
	rxTest:observe( getLocalIdsWithSyncPendingOrErrorObservable ):expectDataWithinTime( 5, { assetLocalId1 } )

end

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

local function buildDocValue( rxTest, wfCatalog, overrides )

	local assetId = KSIdGenerator.uuidInLowercaseHex()

	local docValue, expectedData = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId, overrides )

	return docValue, expectedData, wfCatalog

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAssets_NeedDevelopSettingsUploaded( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					nonZeroStatuses[ k ] = v
				end
			end
			return nonZeroStatuses
		end )

	-- Unsynced Develop Settings

	local overrides = {
		hasMasks = true,
		noDevelopSettingsLink = true
	}

	local docValue = buildDocValue( rxTest, wfCatalog, overrides )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue ) )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsDevelopXmpUpload = 1,
			} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAssets_InvalidAux( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					nonZeroStatuses[ k ] = v
				end
			end
			return nonZeroStatuses
		end )

	-- Unsynced Masks

	local overrides = {
		hasMasks = true,
		noMaskLinks = true,
	}

	local docValue = buildDocValue( rxTest, wfCatalog, overrides )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue ) )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsDevelopXmpUpload = 1,
			} )

	-- Invalid Unsynced Masks

	local docValue2, expectedData = buildDocValue( rxTest, wfCatalog, overrides )

	KSTableUtils.setNested( docValue2, true, '_localOnly', 'files', 'aux', expectedData.mask1Fingerprint, 'error' )
	KSTableUtils.setNested( docValue2, true, '_localOnly', 'files', 'aux', expectedData.mask2Fingerprint, 'error' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue2 ) )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				InvalidAuxError = 1, --docValue2
				NeedsDevelopXmpUpload = 1, --docValue
			} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAssets_InvalidXmpDevelop( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					nonZeroStatuses[ k ] = v
				end
			end
			return nonZeroStatuses
		end )

	-- create asset with unsynced xmp_develop

	local docValue = buildDocValue( rxTest, wfCatalog, { noDevelopSettingsLink = true } )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue ) )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsDevelopXmpUpload = 1,
			} )

	-- mark xmp_develop as invalid

	rxTest:waitFor( wfCatalog:docStore():applyOverlay {
		_id = docValue._id,
		_localOnly = {
			files = {
				xmp_develop = { error = true }
			}
		}
	} )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				InvalidXmpDevelopError = 1,
			} )

end

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

function WFCatSyncStatusTest.rxTestCombinedSyncStatusesColumn( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	local assetSyncStatusList = WFCatSyncStatus.getAssetSyncStatusList()

	local syncStatusesObservable = wfCatalog:indexStore():queryOnce {
			sql = [[ SELECT syncStatuses
					FROM asset_sync_status_ui
					WHERE keepLocallyConfiguration == 0 ]],
			singleColumnQuery = true,
			quiesceUntilUpToDate = true
		}
		:map( function( syncStatuses )

			local combinedStatuses = syncStatuses[ 1 ]

			return WFCatSyncStatus.convertStatusIntegerToStatusTable( combinedStatuses )

		end )

	-- Unsynced Metadata, Original, Develop Settings

	local overrides = {
		noMasterLink = true,
		noDevelopSettingsLink = true,
	}

	local docValue = buildDocValue( rxTest, wfCatalog, overrides )
	docValue._newRevId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue ) )

	local expected =
		{
			'NeedsMetadataUploaded',
			'NeedsOriginalUploaded',
			'NeedsDevelopXmpUpload'
		}

	table.sort( expected )

	rxTest:expectSingleValue( syncStatusesObservable, expected )

	-- Add a few fully synced assets.

	local docValue1 = buildDocValue( rxTest, wfCatalog )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue1 ) )

	local docValue2 = buildDocValue( rxTest, wfCatalog )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue2 ) )

	local docValue3 = buildDocValue( rxTest, wfCatalog )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue3 ) )

	-- Now query for assets in specific states using bitwise operators.

	local sortIds = function( ids )
		table.sort( ids )
		return ids
	end

	local function getAssetsWithStatus( status )
		return wfCatalog:indexStore():queryOnce {
			sql = [[ SELECT assetId
					FROM asset_sync_status_ui
					WHERE keepLocallyConfiguration == 0
						AND syncStatuses & ? ]],
			singleColumnQuery = true,
			quiesceUntilUpToDate = true,
			status
		}
		:map( sortIds )
	end

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.NeedsDevelopXmpUpload ),
			{ docValue._id } )

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.NeedsOriginalUploaded ),
			{ docValue._id } )

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.NeedsMetadataUploaded ),
			{ docValue._id } )

	local combinedStatus = assetSyncStatusList.NeedsDevelopXmpUpload
			+ assetSyncStatusList.NeedsOriginalUploaded

	rxTest:expectSingleValue(
			getAssetsWithStatus( combinedStatus ),
			{ docValue._id } )

	-- Make sure we don't get false positive on other states:

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.Completed ),
			sortIds { docValue1._id, docValue2._id, docValue3._id } )

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.NeedsOriginalDownloaded ),
			{} )

	rxTest:expectSingleValue(
			getAssetsWithStatus( assetSyncStatusList.NeedsDevelopXmpDownloaded ),
			{} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAssets_NeedAuxDownloaded( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					nonZeroStatuses[ k ] = v
				end
			end
			return nonZeroStatuses
		end )

	-- Unsynced Develop Settings

	local overrides = {
		hasMasks = true,
		noLocalMasks = true,
		markedForOfflineUse = true
	}

	local docValue = buildDocValue( rxTest, wfCatalog, overrides )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue ) )

	rxTest:observe( syncStatusObservable )
		:expectDataWithinTime( 5,
			{
				NeedsDevelopXmpDownloaded = 1,
			} )

end

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

function WFCatSyncStatusTest.rxTestGetLocalFileTypeForAsset( rxTest )

	local docValue = {
		_localOnly = {
			files = {
				original = {
					master = {
						relativePath = 'foo',
						volumeGuid = 'bar'
					}
				},
			},
		},
	}

	rxTest:expectEqual( 'OriginalOnDisk', WFCatSyncStatus.translateAssetSyncLocalFileType(
			WFCatSyncStatus._DEBUG_getLocalFileTypeForAsset( docValue ) ) )

	docValue = {
		_localOnly = {
			files = {
				original = {
					master = {
						relativePath = 'foo',
						volumeGuid = 'bar'
					}
				},
				proxy = {
					[ '2560' ] = {
						relativePath = 'foo',
						volumeGuid = 'bar'
					}
				}
			},
		},
	}

	rxTest:expectEqual( 'OriginalOnDisk', WFCatSyncStatus.translateAssetSyncLocalFileType(
			WFCatSyncStatus._DEBUG_getLocalFileTypeForAsset( docValue ) ) )

	docValue = {
		_localOnly = {
			files = {
				proxy = {
					['2560'] = {
						relativePath = 'foo',
						volumeGuid = 'bar'
					}
				}
			},
		},
	}

	rxTest:expectEqual( 'ProxyOnDisk', WFCatSyncStatus.translateAssetSyncLocalFileType(
			WFCatSyncStatus._DEBUG_getLocalFileTypeForAsset( docValue ) ) )

	docValue = {
		_localOnly = {
			files = {
			},
		},
	}

	rxTest:expectEqual( 'NothingOnDisk', WFCatSyncStatus.translateAssetSyncLocalFileType(
			WFCatSyncStatus._DEBUG_getLocalFileTypeForAsset( docValue ) ) )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAlbums( rxTest )

	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local docStore = catalog:docStore()

	local addAlbumFn = WFOzTestUtils.v2.makeAdderOfAlbum( rxTest, docStore )
	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore )

	-- add assets
	local albumId1, albumRevId = addAlbumFn( )
	local assetId1, assetRevId1 = addAssetFn( )
	local assetId2, assetRevId2 = addAssetFn( )

	local statusObservable = rxTest:observe( WFCatSyncStatus.getSyncStatusForAlbums( catalog ) )

	-- newly created, these shouldn't look synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- make it look like it is a 'legacy' album received from Oz with an
	-- empty revision_ids array, this should be counted as 'synced' though
	-- refer to LrAssetUtils.isLatestMetadataUploaded for more details
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						links = {},
						revision_ids = {
						},
					}
				},
		':ignore:' )

	-- album should now appear synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

	-- remove revision_ids and go back to syncing
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						links = {},
						revision_ids = { _delete = true },
					}
				},
		':ignore:' )


	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- make it look like we received the same from oz and processed
	-- with WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						links = {},
						revision_ids = {
							albumRevId
						},
					}
				},
		':ignore:' )

	-- album should now appear synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

	-- change album title

	rxTest:expectOnlyCompleted(
		WFCatAlbums.renameAlbumOrFolder( catalog, 1, 'new_album_name' )
	)

	local updatedAlbumRevId

	rxTest:expectSingleValue( catalog:docStore():docValue( 1 )
		:tap( function( docValue )
			updatedAlbumRevId = docValue._revId
		end ), ':ignore:' )

	-- album should appear to be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- fake WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						revision_ids = {
							albumRevId,
							updatedAlbumRevId,
						}
					}
				},
		':ignore:' )

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

	-- add some assets to the album
	rxTest:expectOnlyCompleted(
		WFCatAlbums.addAssetsToAlbum( catalog, 1, { assetId1, assetId2 } )
	)

	-- since assets aren't uploaded, album should be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- complete first asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId1,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId1
						}
					}
				},
		':ignore:' )

	-- album should still be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  	-- album 1
		} )

	-- complete second asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId2,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId2
						}
					}
				},
		':ignore:' )

	-- album should now be complete
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAlbums_IgnoreAlbumStatus( rxTest )

	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local docStore = catalog:docStore()

	local addAlbumFn = WFOzTestUtils.v2.makeAdderOfAlbum( rxTest, docStore )
	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore )

	-- add assets
	local albumId1 = addAlbumFn( )
	local assetId1, assetRevId1 = addAssetFn( )
	local assetId2, assetRevId2 = addAssetFn( )

	local statusObservable = rxTest:observe( WFCatSyncStatus.getSyncStatusForAlbums( catalog, true ) )

	-- when ignoring album status, an album should immediately be considered synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  -- album 1
		} )

	-- change album title

	rxTest:expectOnlyCompleted(
		WFCatAlbums.renameAlbumOrFolder( catalog, 1, 'new_album_name' )
	)

	local updatedAlbumRevId

	rxTest:expectSingleValue( catalog:docStore():docValue( 1 )
		:tap( function( docValue )
			updatedAlbumRevId = docValue._revId
		end ), ':ignore:' )

	-- add some assets to the album
	rxTest:expectOnlyCompleted(
		WFCatAlbums.addAssetsToAlbum( catalog, 1, { assetId1, assetId2 } )
	)

	-- since assets aren't uploaded, album should be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- complete first asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId1,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId1
						}
					}
				},
		':ignore:' )

	-- album should still be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  	-- album 1
		} )

	-- complete second asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId2,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId2
						}
					}
				},
		':ignore:' )

	-- album should now be complete
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForOfflineAlbums( rxTest )

	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local keptLocalInfo = {
		keepAllOriginals = true,
		keepAllProxies = true,
	}

	local originalsPathInfo = catalog:fileSpec():originalsPathInfo()
	local volumeGuid = originalsPathInfo.volumeGuid
	local relativePath = originalsPathInfo.relativePath

	local volumeAgent = WFCatVolumeAgent( catalog, relativePath, volumeGuid, nil, nil, keptLocalInfo )
	volumeAgent:setInternetConnectionStatus( true )

	catalog:addVolumeAgent( volumeAgent )

	local docStore = catalog:docStore()

	local addAlbumFn = WFOzTestUtils.v2.makeAdderOfAlbum( rxTest, docStore )
	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore )

	-- add assets
	local albumId1, _ = addAlbumFn( )
	local assetId1, assetRevId1 = addAssetFn( )
	local assetId2, assetRevId2 = addAssetFn( )

	local statusObservable = rxTest:observe( WFCatSyncStatus.getSyncStatusForAlbums( catalog ) )

	-- newly created, these shouldn't look synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- Mark the album for offline use.

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

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncingForOfflineUse',  -- album 1
		} )

	-- make it look like it is a 'legacy' album received from Oz with an
	-- empty revision_ids array, this should be counted as 'synced' though
	-- refer to LrAssetUtils.isLatestMetadataUploaded for more details
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						links = {},
						revision_ids = {
						},
					}
				},
		':ignore:' )

	-- album should now appear synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncedForOfflineUse',  	-- album 1
		} )

	-- add some assets to the album

	rxTest:expectOnlyCompleted(
		WFCatAlbums.addAssetsToAlbum( catalog, 1, { assetId1, assetId2 } )
	)

	-- since assets aren't uploaded, album should be syncing

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncingForOfflineUse',  -- album 1
		} )

	-- complete first asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId1,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId1
						}
					},
					_localOnly = {
						isMarkedForOfflineUse = true
					}
				},
		':ignore:' )

	-- album should still be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncingForOfflineUse',  	-- album 1
		} )

	-- complete second asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId2,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId2
						}
					},
					_localOnly = {
						isMarkedForOfflineUse = true
					}
				},
		':ignore:' )

	-- album should now be complete
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncedForOfflineUse',  	-- album 1
		} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForOfflineAlbums_IgnoreAlbumStatus( rxTest )

	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local keptLocalInfo = {
		keepAllOriginals = true,
		keepAllProxies = true,
	}

	local originalsPathInfo = catalog:fileSpec():originalsPathInfo()
	local volumeGuid = originalsPathInfo.volumeGuid
	local relativePath = originalsPathInfo.relativePath

	local volumeAgent = WFCatVolumeAgent( catalog, relativePath, volumeGuid, nil, nil, keptLocalInfo )
	volumeAgent:setInternetConnectionStatus( true )

	catalog:addVolumeAgent( volumeAgent )

	local docStore = catalog:docStore()

	local addAlbumFn = WFOzTestUtils.v2.makeAdderOfAlbum( rxTest, docStore )
	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore )

	-- add assets
	local albumId1, _ = addAlbumFn( )
	local assetId1, assetRevId1 = addAssetFn( )
	local assetId2, assetRevId2 = addAssetFn( )

	local statusObservable = rxTest:observe( WFCatSyncStatus.getSyncStatusForAlbums( catalog, true ) )

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'synced',  -- album 1
		} )

	-- Mark the album for offline use.

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

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncedForOfflineUse',  -- album 1
		} )

	-- make it look like it is a 'legacy' album received from Oz with an
	-- empty revision_ids array, this should be counted as 'synced' though
	-- refer to LrAssetUtils.isLatestMetadataUploaded for more details
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = 1,
					_serverProvidedMetadata = {
						links = {},
						revision_ids = {
						},
					}
				},
		':ignore:' )

	-- album should now appear synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncedForOfflineUse',  	-- album 1
		} )

	-- add some assets to the album

	rxTest:expectOnlyCompleted(
		WFCatAlbums.addAssetsToAlbum( catalog, 1, { assetId1, assetId2 } )
	)

	-- since assets aren't uploaded, album should be syncing

	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncingForOfflineUse',  -- album 1
		} )

	-- complete first asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId1,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId1
						}
					},
					_localOnly = {
						isMarkedForOfflineUse = true
					}
				},
		':ignore:' )

	-- album should still be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncingForOfflineUse',  	-- album 1
		} )

	-- complete second asset by faking WFCatMetadataPuller
	rxTest:expectSingleValue(
		docStore:applyOverlay {
					_id = assetId2,
					_serverProvidedMetadata = {
						links = {
							["/rels/master"] = { href = "something" }
						},
						revision_ids = {
							assetRevId2
						}
					},
					_localOnly = {
						isMarkedForOfflineUse = true
					}
				},
		':ignore:' )

	-- album should now be complete
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncedForOfflineUse',  	-- album 1
		} )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusForAlbums_testNightly( rxTest )

	rxTest:setTimeLimit( 80 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog

	rxTest:expectSingleValue(

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

		':ignore:' )

	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:waitFor( pusher:sendUpdatesContinuously() )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	puller:getUpdatesContinuously()

	catalog:addCloseAction( function()
		pusher:stopContinuousUpdates()
		puller:stopContinuousUpdates()
	end )

	local docStore = catalog:docStore()

	local addRealId = function( docValue )
		docValue._id = KSIdGenerator.uuidInLowercaseHex()
		return docValue
	end

	local addAlbumFn = WFOzTestUtils.v2.makeAdderOfAlbum( rxTest, docStore, addRealId )
	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore, addRealId )

	-- add assets
	local albumId1 = addAlbumFn( )
	local assetId1 = addAssetFn( )

	local statusObservable = rxTest:observe( WFCatSyncStatus.getSyncStatusForAlbums( catalog ) )

	-- newly created, these shouldn't look synced
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- album should now appear synced
	statusObservable:expectDataWithinTime( 40,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

	-- change album title
	rxTest:expectOnlyCompleted(
		WFCatAlbums.renameAlbumOrFolder( catalog, 1, 'new_album_name' )
	)

	-- album should appear to be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	statusObservable:expectDataWithinTime( 40,
		{
			[albumId1] = 'synced',  	-- album 1
		} )

	-- add an assets to the album
	rxTest:expectOnlyCompleted(
		WFCatAlbums.addAssetsToAlbum( catalog, 1, { assetId1 } )
	)

	-- since assets aren't uploaded, album should be syncing
	statusObservable:expectDataWithinTime( 5,
		{
			[albumId1] = 'syncing',  -- album 1
		} )

	-- Without fully uploading an asset it's hard to test that the status
	-- is complete when assets are complete. We could hack the doc store with
	-- an overlay, but that's not really different than the non-nightly tests

end

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

function WFCatSyncStatusTest.rxTestInitialSyncCompleted( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )

	local obs = rxTest:observe( WFCatSyncStatus.isDoingInitialSync( wfCatalog ) )

	obs:expectDataWithinTime( 3, true )

	rxTest:expectSingleValue(
		wfCatalog:docStore():setLocalDocValue {
			_id = 'Oz-isDoingInitialSync',
			isDoingInitialSync = true,
		}, true )

	obs:expectDataWithinTime( 3, true )

	rxTest:expectSingleValue(
		wfCatalog:docStore():setLocalDocValue {
			_id = 'Oz-isDoingInitialSync',
			isDoingInitialSync = false,
		}, true )

	obs:expectDataWithinTime( 3, false )

end

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

local function observeIsOnOz( rxTest, catalog, docId )

	return rxTest:observe( catalog:docStore():ongoingDocValue( docId )
		:map( function( docValue )

			local isLabeledOz = not not ( docValue._labels and docValue._labels.oz )

			return isLabeledOz

		end )
	)

end

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

function WFCatSyncStatusTest.rxTestIsMetadataUpload_testNightly( rxTest )

	rxTest:setTimeLimit( 80 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog

	rxTest:expectSingleValue(

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

		':ignore:' )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	local pusher = WFCatMetadataPusher( catalog, options )

	puller:getUpdatesContinuously()

	catalog:addCloseAction( function()
		pusher:close()
		puller:close()
	end )

	local docStore = catalog:docStore()

	local addRealId = function( docValue )
		docValue._id = KSIdGenerator.uuidInLowercaseHex()
		return docValue
	end

	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore, addRealId )

	-- add assets
	local assetId1 = addAssetFn()

	-- Send changes to Oz

	local sendNUpdates = rxTest:observe( pusher:_DEBUG_sendNUpdates( 1 ) )
	sendNUpdates:expectCompletedWithinTime( 30 )
	sendNUpdates:dispose()

	-- Verify that the asset status is set to metadata upload pending
	rxTest:expectSingleValue(

		catalog:docStore():docValue( assetId1 )
			:map( function( docValue )

				local assetSyncStatus = WFCatSyncStatus._DEBUG_getAssetStatus( docValue )

				return assetSyncStatus

			end ), 1 )

	local observeAssetIsOnOz = observeIsOnOz( rxTest, catalog, assetId1 )
	observeAssetIsOnOz:expectDataWithinTime( 40, true )

	-- Verify the asset status is no longer in metadata upload pending

	rxTest:expectSingleValue(

		catalog:docStore():docValue( assetId1 )
			:map( function( docValue )

				local assetSyncStatus = WFCatSyncStatus._DEBUG_getAssetStatus( docValue )

				return assetSyncStatus

			end ), 2 )

	-- Send another change to Oz

	rxTest:expectSingleValue(

		catalog:docStore():applyOverlay {
			_id = assetId1,
			importSource = {
				originalWidth = 100,
			}
		}, ':ignore:' )

	observeAssetIsOnOz:expectDataWithinTime( 40, false )

	sendNUpdates = rxTest:observe( pusher:_DEBUG_sendNUpdates( 1 ) )
	sendNUpdates:expectCompletedWithinTime( 30 )
	sendNUpdates:dispose()

	-- Verify again the asset metadata needs to be uploaded

	rxTest:expectSingleValue(

		catalog:docStore():docValue( assetId1 )
			:map( function( docValue )

				local assetSyncStatus = WFCatSyncStatus._DEBUG_getAssetStatus( docValue )

				return assetSyncStatus

			end ), 1 )

	observeAssetIsOnOz:expectDataWithinTime( 40, true )

	-- Asset status is no longer metadata upload pending

	rxTest:expectSingleValue(

		catalog:docStore():docValue( assetId1 )
			:map( function( docValue )

				local assetSyncStatus = WFCatSyncStatus._DEBUG_getAssetStatus( docValue )

				return assetSyncStatus

			end ), 2 )

end

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

function WFCatSyncStatusTest.rxTestGetSyncStatusOfAssetsOfVersions( rxTest )

	-- [jandrew 2022-01-04]: WF-1644: The following test verifies reduceSyncStatuses,
	-- which reduces asset sync states into a single is-document-synced boolean.
	rxTest:setTimeLimit( 30 )

	local catalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( catalog )

	local docStore = catalog:docStore()

	local addAssetFn = WFOzTestUtils.v2.makeAdderOfAsset( rxTest, docStore, function( docValue )
			return docValue
		end )

	local assetId1, assetRevId1 = addAssetFn()

	local versionId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:waitFor(
		docStore:setDocValue {
			_id = versionId,
			asset = {
				id = assetId1,
			},
			type = "version",
			subtype = "edit",
		} )

	local versionLocalId = rxTest:getFirstValue( docStore:docIdToLocalId( versionId ) )

	local reducedSyncStatusObserver = rxTest:observe(
			WFCatSyncStatus.getSyncStatusOfAssetsOfVersions( catalog, { versionLocalId } ) )

	--- Initial asset is in a needs-metadata-sync state.

	reducedSyncStatusObserver:expectDataWithinTime( 5,  { [ assetId1 ] = false } )

	-- Make it look like the document is synced.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_serverProvidedMetadata = {
				links = {
					["/rels/master"] = { href = "something" },
					["/rels/proxy_type/2560"] = { href = "something" }
				},
				revision_ids = {
					assetRevId1
				}
			}
		} )

	reducedSyncStatusObserver:expectDataWithinTime( 5,  { [ assetId1 ] = true } )

	-- Make it look the asset can't be uploaded because we ran out of storage on oz.

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId1,
			_localOnly = {
				ozError = {
					subtype = 'StorageExceededError',
				}
			},
			name = 'foo',
			_serverProvidedMetadata = { _deleted = true }
		} )

	reducedSyncStatusObserver:expectDataWithinTime( 5,  { [ assetId1 ] = false } )

end

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

function WFCatSyncStatusTest.rxTestGetBlockedSpaceIdsMapStream( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	-- Create a few spaces.

	local baseSpaceDocValue = {
		type = 'space',
		subtype = 'shared',
		private = false,
		download = false,
		updated = KSDate.timeInIso8601Format(),
		created = KSDate.timeInIso8601Format(),
	}

	-- Space 1 = valid

	local spaceId1 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = spaceId1

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Space 2 = entitlement expired

	local blockedSpaceId1 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = blockedSpaceId1
	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Space 3 = under maintenance

	local blockedSpaceId2 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = blockedSpaceId2
	KSTableUtils.setNested( baseSpaceDocValue, false, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )
	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'maintenanceLock' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Make sure we correctly detect the blocked spaces.

	local observer = rxTest:observe( WFCatSyncStatus.getBlockedSpaceIdsMapStream( wfCatalog ) )

	local expectedResult = {
		[ blockedSpaceId1 ] = true,
		[ blockedSpaceId2 ] = true
	}

	observer:expectDataWithinTime( 5, expectedResult )

end

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

local function getBaseSpaceDocValue( optSpaceId )

	return {
		_id = optSpaceId or KSIdGenerator.uuidInLowercaseHex(),
		type = 'space',
		subtype = 'shared',
		private = false,
		download = false,
		updated = KSDate.timeInIso8601Format(),
		created = KSDate.timeInIso8601Format(),
	}

end

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

function WFCatSyncStatusTest.rxTestGetBlockedSpaceIdsMapStream( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	-- Create a few spaces.

	local baseSpaceDocValue = getBaseSpaceDocValue()

	-- Space 1 = valid

	local spaceId1 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = spaceId1

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Space 2 = entitlement expired

	local blockedSpaceId1 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = blockedSpaceId1
	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Space 3 = under maintenance

	local blockedSpaceId2 = KSIdGenerator.uuidInLowercaseHex()
	baseSpaceDocValue._id = blockedSpaceId2
	KSTableUtils.setNested( baseSpaceDocValue, false, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )
	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'maintenanceLock' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Make sure we correctly detect the blocked spaces.

	local observer = rxTest:observe( WFCatSyncStatus.getBlockedSpaceIdsMapStream( wfCatalog ) )

	local expectedResult = {
		[ blockedSpaceId1 ] = true,
		[ blockedSpaceId2 ] = true
	}

	observer:expectDataWithinTime( 5, expectedResult )

	observer:dispose()

end

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

function WFCatSyncStatusTest.rxTestSyncPendingForAnyAsset( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	-- Create a few assets.

	-- Asset 1 = Not Synced

	local options = {
		noMasterLink = true
	}

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local docValue1 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId1, options )
	docValue1._spaceId = spaceId

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue1 ) )

	-- Asset 2 = Fully Synced

	local assetId2 = KSIdGenerator.uuidInLowercaseHex()
	local docValue2 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId2 )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue2 ) )

	-- Verify sync is pending.

	local observer = rxTest:observe( WFCatSyncStatus.syncPendingForAnyAsset( wfCatalog ) )

	observer:expectDataWithinTime( 5, true )

	-- Add Asset 1 to a blocked space. It should no longer impact pending sync.

	local baseSpaceDocValue = getBaseSpaceDocValue( spaceId )

	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Verify sync is not pending.

	observer:expectDataWithinTime( 5, false )

	observer:dispose()

end

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

local function addBinaryDownloadError( docValue, rels )

	local href = KSTableUtils.derefOpt(
			docValue, '_serverProvidedMetadata', 'links', rels, 'href' )

	KSTableUtils.setNested(
			docValue, true, '_localOnly', 'binaryDownloadErrors', rels, href )

end

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

local function getLocalId( rxTest, wfCatalog, assetId )

	return rxTest:getFirstValue( wfCatalog:docStore():docValue( assetId )
		:pluck( "_localId" ) )

end

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

function WFCatSyncStatusTest.rxTestGetLocalIdsWithBinaryDownloadErrorsForSpace( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	-- Helper Functions

	local function sortLocalIds( localIds )
		table.sort( localIds )
		return localIds
	end

	-- Create a few assets in different error states.

	-- Asset 1 = No Errors

	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	local options = {
		noLocalOriginal = true
	}

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local docValue1 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId1, options )
	docValue1._spaceId = spaceId

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue1 ) )

	-- Asset 2 = Original Download Error

	local assetId2 = KSIdGenerator.uuidInLowercaseHex()
	local docValue2 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId2, options )
	docValue2._spaceId = spaceId

	addBinaryDownloadError( docValue2, '/rels/master' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue2 ) )

	local doc2LocalId = getLocalId( rxTest, wfCatalog, assetId2 )

	-- Asset 3 = Proxy Download Error

	local assetId3 = KSIdGenerator.uuidInLowercaseHex()
	local docValue3 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId3, options )
	docValue3._spaceId = spaceId

	addBinaryDownloadError( docValue3, '/rels/proxy_type/2560' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue3 ) )

	local doc3LocalId = getLocalId( rxTest, wfCatalog, assetId3 )

	-- Asset 4 = XMP Download Error

	local assetId4 = KSIdGenerator.uuidInLowercaseHex()
	local docValue4 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId4, options )
	docValue4._spaceId = spaceId

	addBinaryDownloadError( docValue4, '/rels/xmp/develop' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue4 ) )

	local doc4LocalId = getLocalId( rxTest, wfCatalog, assetId4 )

	-- Verify docs with binary errors in the specified space.

	rxTest:expectSingleValue(
			WFCatSyncStatus.getLocalIdsWithBinaryDownloadErrorsForSpace( wfCatalog, spaceId )
			:map( sortLocalIds ),
			{ doc2LocalId, doc3LocalId, doc4LocalId } )

	rxTest:expectSingleValue(
			WFCatSyncStatus.getLocalIdsWithBinaryDownloadErrorsForSpace( wfCatalog, "fakeSpaceId" ),
			{} )

end

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

function WFCatSyncStatusTest.rxTestGetDocIdsAndStatusWithSyncPendingOrError( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

	-- Create a few assets in different states.

	-- Asset 1 = Synced

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local docValue1 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId1 )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue1 ) )

	-- Asset 2 = Not Synced

	local options = {
		noMasterLink = true
	}

	local assetId2 = KSIdGenerator.uuidInLowercaseHex()
	local docValue2 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId2, options )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue2 ) )

	-- Asset 3 = Error

	local assetId3 = KSIdGenerator.uuidInLowercaseHex()
	local docValue3 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId3 )

	addBinaryDownloadError( docValue3, '/rels/proxy_type/2560' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue3 ) )

	-- Asset 4 = Not Synced But in Blocked Space
	-- Assets in blocked spaces should not be returned by getDocIdsAndStatusWithSyncPendingOrError

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local assetId4 = KSIdGenerator.uuidInLowercaseHex()
	local docValue4 = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId4, options )
	docValue4._spaceId = spaceId

	rxTest:waitFor( wfCatalog:docStore():setDocValue( docValue4 ) )

	local baseSpaceDocValue = getBaseSpaceDocValue( spaceId )

	KSTableUtils.setNested( baseSpaceDocValue, true, '_serverProvidedMetadata', 'owner', 'entitlementExpired' )

	rxTest:waitFor( wfCatalog:docStore():setDocValue( baseSpaceDocValue ) )

	-- Verify sync is pending.

	local observer = rxTest:observe( WFCatSyncStatus.getDocIdsAndStatusWithSyncPendingOrError( wfCatalog )

		:map( function( results )

			table.sort( results, function( a, b )
				return a.status < b.status
			end)

			return results

		end ) )

	local assetSyncStatusList = WFCatSyncStatus.getAssetSyncStatusList()

	local expectedData = {
		{
			assetId = assetId2,
			status = assetSyncStatusList.NeedsOriginalUploaded,
			statusString = 'NeedsOriginalUploaded',
		},
		{
			assetId = assetId3,
			status = assetSyncStatusList.ProxyDownloadError,
			statusString = 'ProxyDownloadError',
		}
	}

	observer:expectDataWithinTime( 5, expectedData )

	observer:dispose()

end

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

function WFCatSyncStatusTest.rxTestBinaryUploadDisparityFinder_testNightly( rxTest )

	-- The following test was written to identify disparities in the implementations
	-- of the logic that is used to identify which assets have originals that
	-- need to be synced to Oz. Historically, we've seen issues here between the
	-- sync status code and the binary upload code.

	rxTest:setTimeLimit( 30 )

	local wfCatalog = createFakeWFCatalog( rxTest )
	rxTest:addCloseTarget( wfCatalog )

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

	local syncStatusObservable = WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )
		:map( function( statuses )
			local nonZeroStatuses = {}
			for k, v in pairs( statuses ) do
				if v ~= 0 then
					if k == 'NeedsOriginalUploaded' then
						nonZeroStatuses[ k ] = v
					else
						local count = nonZeroStatuses[ 'Other' ] or 0
						nonZeroStatuses[ 'Other' ] = count + v
					end
				end
			end
			return nonZeroStatuses
		end )

	local needsBinaryUploadQuery = indexStore:queryOnce( {
			sql = [[ SELECT DISTINCT docId
						FROM needsBinaryUpload ]],
			singleColumnQuery = true
		} )
		:map( function( docIds )
			rxTest:expectEqual( true, #docIds <= 1 )
			return docIds[ 1 ]
		end )

	-- Function to verify no disparities between needsBinaryUpload table
	-- and the WFCatSyncStatus code.

	local function verifySyncStatusAndBinaryUploader( docValueOptions )

		local docValue = buildDocValue( rxTest, wfCatalog, docValueOptions )
		rxTest:waitFor( docStore:setDocValue( docValue ) )

		local needsBinaryUpload = rxTest:getFirstValue( needsBinaryUploadQuery )

		-- Verify WFCatSyncStatus and WFCatBinaryUploader

		if needsBinaryUpload then

			rxTest:observe( syncStatusObservable )
				:expectDataWithinTime( 3,
					{
						NeedsOriginalUploaded = 1,
					} )

		else

			rxTest:observe( syncStatusObservable )
				:expectDataWithinTime( 3,
					{
						Other = 1,
					} )

		end

		-- Delete asset

		rxTest:waitFor( docStore:deleteDoc( docValue._id ) )

	end

	-- Test All Permutations of Binary Upload Sync States

	local ozOriginalStates = {
		'hasOriginalLink',
		'noOriginalLink',
		'originalLinkIsInvalid'
	}

	local localOriginalStates = {
		'hasLocalOriginal',
		'noLocalOriginal',
		'hasCorruptOriginal',
		'hasInvalidOriginal',
		'localOriginalIsOnDisconnectedVolume',
	}

	local copyStates = {
		'doesNotHaveOriginalSourceLinkForCopy',
		'hasOriginalSourceLinkForCopy',
		'isCopy'
	}

	local docValueOptionsBase = {}

	for _, ozOriginalState in ipairs( ozOriginalStates ) do

		local ozOriginalModifiers = {}

		if ozOriginalState == 'hasOriginalLink' then
			-- noop
		elseif ozOriginalState == 'noOriginalLink' then
			ozOriginalModifiers.noMasterLink = true
		elseif ozOriginalState == 'originalLinkIsInvalid' then
			ozOriginalModifiers.ozOriginalIsInvalid = true
		end

		for _, localOriginalState in ipairs( localOriginalStates ) do

			local localOriginalModifiers = {}

			if localOriginalState == 'hasLocalOriginal' then
				-- noop
			elseif localOriginalState == 'noLocalOriginal' then
				localOriginalModifiers.noLocalOriginal = true
			elseif localOriginalState == 'hasCorruptOriginal' then
				localOriginalModifiers.corruptMaster = true
			elseif localOriginalState == 'hasInvalidOriginal' then
				localOriginalModifiers.localOriginalIsInvalid = true
			elseif localOriginalState == 'localOriginalIsOnDisconnectedVolume' then
				localOriginalModifiers.localOriginalIsOnDisconnectedVolume = true
			end

			for _, copyState in ipairs( copyStates ) do

				local copyStateModifiers = {}

				if copyState == 'doesNotHaveOriginalSourceLinkForCopy' then
					-- noop
				elseif copyState == 'hasOriginalSourceLinkForCopy' then
					copyStateModifiers.hasOriginalSourceLinkForCopy = true
				elseif copyState == 'isCopy' then
					copyStateModifiers.isCopy = true
				end

				local docValueOptions = {}

				docValueOptions = WFTableMerge.applyOverlay(
						docValueOptions, docValueOptionsBase )

				docValueOptions = WFTableMerge.applyOverlay(
						docValueOptions, ozOriginalModifiers )

				docValueOptions = WFTableMerge.applyOverlay(
						docValueOptions, localOriginalModifiers )

				docValueOptions = WFTableMerge.applyOverlay(
						docValueOptions, copyStateModifiers )

				verifySyncStatusAndBinaryUploader( docValueOptions )

			end

		end

	end

end

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

if WFPlatformInfo.isDesktop then
	return WFRxTestCase.wrapSuite( 'WFCatSyncStatusTest', WFCatSyncStatusTest )
else
	return {}
end
