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

WFCatStuckSyncUtilsTest.lua

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

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

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

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

	-- RxLua
local Rx = require 'Rx'

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.managed_catalog
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatStuckSyncUtils = require 'WFCatStuckSyncUtils'
local WFCatSyncStatus = require 'WFCatSyncStatus'

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

local function createFakeWFCatalog( rxTest )

	local optArgs = {}
		
	if WFPlatformInfo.isMobile() then
		optArgs.indexDefinitions = 'LrmCatalogIndexDefinitions.lua'
	end

	return WFCatMockCatalog.createFakeWFCatalog( rxTest , nil, nil, optArgs )

end

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

local function sortStuckDocIds( assetIds )

	table.sort( assetIds )
	
	return assetIds

end

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

local WFCatStuckSyncUtilsTest = {}

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

local function addXmpDevelopTempLinkToDocAndRemoveDevelopLink( catalog, assetId, sha256 )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/temp/xmp/develop/' .. sha256 ] = {
					exists = true,
				},
				[ '/rels/xmp/develop' ] = { _delete = true }
			},
		},
	}

end

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

local function addXmpDevelopLinkToDocAndRemoveTempLink( catalog, assetId, sha256 )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp/develop' ] = {
					href = 'this/does/not/matter/for/these/tests',
					sha256 = sha256,
					exists = true
				},
				[ '/rels/temp/xmp/develop/' .. sha256 ] = { _delete = true }
			},
		},
	}

end

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

local function addAuxTempLinkToDoc( catalog, assetId, auxSha256 )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/temp/aux/' .. auxSha256 ] = {
					exists = true,
				},
			},
		},
	}

end

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

local function testGetDocsStuckSyncingMetadataStream( rxTest, isSpace, atomicDevelopSettings )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )

	local docStore = wfCatalog:docStore()

	-- Start watching for stuck documents

	local isNormalSyncRunningStream = Rx.BehaviorSubject( true )
	local calm = 0.1
	local stuckReportingTimeThreshold = 0

	local capabilities = {
		[ 'WFCatBinaryUploader.atomicDevelopSettings' ] = atomicDevelopSettings,
	}
	local stuckSyncObserver = rxTest:observe( 
			WFCatStuckSyncUtils.getDocsStuckSyncingMetadataStream( wfCatalog,
					isNormalSyncRunningStream, calm, stuckReportingTimeThreshold, capabilities )
				:map( sortStuckDocIds ) )

	-- Create a catalog asset w/ unsynced develop settings.

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

	local docId = KSIdGenerator.uuidInLowercaseHex()

	local docValue, docExpectedData
			= WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, docId, overrides )

	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	if isSpace then
		docValue._spaceId = spaceId
	end

	rxTest:waitFor( docStore:setDocValue( docValue ) )
		-- The initial revision is fully synced

	-- Make the doc appear unsynced:

	local overlay = {
		_id = docId,
		_newRevId = KSIdGenerator.uuidInLowercaseHex(),
	}

	rxTest:waitFor( docStore:applyOverlay( overlay ) )

	-- Make sure the assets are in a needs metadata uploaded state:

	local assetSyncStatusList = WFCatSyncStatus.getAssetSyncStatusList()

	local assetsNeedingMetadataUploadSql

	if WFPlatformInfo.isMobile() then -- iOS & Android

		assetsNeedingMetadataUploadSql =
				[[ SELECT docId
					FROM coreInfo
					WHERE syncStatus == 1 ]]

	else -- Desktop

		assetsNeedingMetadataUploadSql =
				[[ SELECT assetId 
					FROM asset_sync_status_ui
					WHERE keepLocallyConfiguration == ?
						AND syncStatuses & ? ]]

	end

	rxTest:expectSingleValue( wfCatalog:indexStore():queryOnce( {
			sql = assetsNeedingMetadataUploadSql,
			quiesceUntilUpToDate = true,
			singleColumnQuery = true,
			WFCatSyncStatus.getKeepLocallyConfiguration(),
			assetSyncStatusList.NeedsMetadataUploaded
		} ):map( sortStuckDocIds ), 
		sortStuckDocIds( { docId } ) )
	
	-- And that there's no stuck sync.
	
	stuckSyncObserver:expectDataWithinTime( 5, {} )

	-- Disable Sync

	isNormalSyncRunningStream:onNext( false )

	-- Now make it look like the catalog pusher skipped the catalog asset.
	--
	-- Documents are only considered stuck if the the change counter associated
	-- with the document revision is behind the change counter associated with
	-- the metadata pusher.
	--
	-- To make a document appear stuck in the test, we have to bump up the change
	-- counter of the metadata pusher so that it's greater than the change counter
	-- of the document. futureContinueFrom is this calculated change counter value.
	--
	-- changeCounterOffset adjusts the value of futureContinueFrom based on whether
	-- or not ADS is enabled. With ADS enabled, there's logic in the test that
	-- applies additional overlays to the document. The additional overlays result
	-- in the document having a larger change counter, and therefore we need to move
	-- the metadata pusher even further ahead.

	local changeCounterOffset = 1
		-- 1 ahead of the current change counter

	if atomicDevelopSettings then

		changeCounterOffset = 5
			-- 3 ahead of the current change counter to account for the docStore
			-- operations inside of the "if atomicDevelopSettings" block below.

	end

	local futureContinueFrom = rxTest:getFirstValue( docStore:changeCounterOnce() ) + changeCounterOffset

	local catalogPusherLocalDocValue = {
		_id = WFCatMetadataPusher.getContinueFromLocalDocValueKey( isSpace ),
		continueFrom = futureContinueFrom
	}

	rxTest:waitFor( docStore:setLocalDocValue( catalogPusherLocalDocValue ) )

	-- Verify there's no stuck sync because sync is disabled

	stuckSyncObserver:expectDataWithinTime( 5, {} )

	-- Enable sync

	isNormalSyncRunningStream:onNext( true )

	if atomicDevelopSettings then

		-- There's no stuck sync because the asset has unsynced ADS binaries.

		stuckSyncObserver:expectDataWithinTime( 5, {} )

		-- Make the ADS develop settings appear synced via a temp link

		rxTest:waitFor( addXmpDevelopTempLinkToDocAndRemoveDevelopLink(
				wfCatalog, docId, docExpectedData.developSettingsSha256 ) )

		-- Because of the unsynced auxiliaries, there's still no stuck sync:

		stuckSyncObserver:expectDataWithinTime( 5, {} )

		-- Make the auxiliaries appear synced:

		rxTest:waitFor( addAuxTempLinkToDoc(
				wfCatalog, docId, docExpectedData.mask1Sha256 ) )

		rxTest:waitFor( addAuxTempLinkToDoc(
				wfCatalog, docId, docExpectedData.mask2Sha256 ) )

		-- Now it should appear that we have a stuck sync.

		stuckSyncObserver:expectDataWithinTime( 5, { docId } )

		-- Remove the temp link, and make the binaries appear synced via a confirmed link

		rxTest:waitFor( addXmpDevelopLinkToDocAndRemoveTempLink(
			wfCatalog, docId, docExpectedData.developSettingsSha256 ) )

		-- It should still appear that we have a stuck sync.

		stuckSyncObserver:expectDataWithinTime( 5, { docId } )

	else

		-- Now it should appear that we have a stuck sync.

		stuckSyncObserver:expectDataWithinTime( 5, { docId } )

	end

	-- Now attempt to resolve the stuck sync by bumping up the change counters (AKA forcing a new
	-- revision ID):

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId } ) )

	-- Verify the new revision ID and looper-protection annotation:

	local catalogAssetRevId = rxTest:getFirstValue( docStore:docValue( docId ):pluck( '_revId' ) )
	rxTest:expectSingleValue( docStore:docValue( docId ):pluck( '_localOnly', 'stuckSyncUtils', 'lastGeneratedRevisionId' ), catalogAssetRevId )

	-- Looper protection should prevent us from quickly generating new revision IDs via resolveStuckSync:

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId } ) )

	rxTest:expectSingleValue( docStore:docValue( docId ):pluck( '_revId' ), catalogAssetRevId )
	rxTest:expectSingleValue( docStore:docValue( docId ):pluck( '_localOnly', 'stuckSyncUtils', 'lastGeneratedRevisionId' ), catalogAssetRevId )

	-- Verify that they are no longer stuck:

	stuckSyncObserver:expectDataWithinTime( 5, {} )

	stuckSyncObserver:dispose()

	isNormalSyncRunningStream:dispose()

end

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

function WFCatStuckSyncUtilsTest.rxTestGetDocsStuckSyncingMetadataStream( rxTest )

	testGetDocsStuckSyncingMetadataStream( rxTest )

end

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

function WFCatStuckSyncUtilsTest.rxTestGetDocsStuckSyncingMetadataStream_SpaceAsset( rxTest )

	local isSpace = true

	testGetDocsStuckSyncingMetadataStream( rxTest, isSpace )

end

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

function WFCatStuckSyncUtilsTest.rxTestGetDocsStuckSyncingMetadataStream_AtomicDevelopSettings( rxTest )

	local atomicDevelopSettings = true

	local isSpace = false

	testGetDocsStuckSyncingMetadataStream( rxTest, isSpace, atomicDevelopSettings )

end

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

function WFCatStuckSyncUtilsTest.rxTestGetDocsStuckSyncingMetadataStream_AtomicDevelopSettings_SpaceAsset( rxTest )

	local atomicDevelopSettings = true

	local isSpace = true

	testGetDocsStuckSyncingMetadataStream( rxTest, isSpace, atomicDevelopSettings )

end

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

function WFCatStuckSyncUtilsTest.rxTestGetDocsStuckSyncingMetadataStream_unconfirmedDeletion( rxTest )

	rxTest:setTimeLimit( 10 )

	local wfCatalog = createFakeWFCatalog( rxTest )

	-- Start watching for stuck documents

	local isNormalSyncRunningStream = Rx.BehaviorSubject( true )

	local stuckSyncObserver = rxTest:observe( 
			WFCatStuckSyncUtils.getDocsStuckSyncingMetadataStream( wfCatalog, isNormalSyncRunningStream, 0.1, 0 )
			:map( sortStuckDocIds ) )

	-- create an error document which should be ignored.
	local errorDocId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:expectSingleValue(
		wfCatalog:docStore():setDocValue {
			_id = errorDocId,
			subtype = "ThisIsAnUnknownErrorSubtype",
			type = "error",
			_serverProvidedMetadata = {
				errors = {
					asset = {
						"unknown error",
					},
				},
				source = {
					action = "create",
					id = KSIdGenerator.uuidInLowercaseHex(),
					revision_id = KSIdGenerator.uuidInLowercaseHex(),
					type = "asset",
				},
				status = 400,
				updated = "2016-06-07T15:55:34.218872Z",
			},
		},
		':ignore:' )

	-- Create an asset that has never synced to Oz.

	local neverSyncedCatalogAssetId = KSIdGenerator.uuidInLowercaseHex()
	local neverSyncedcatalogDocValue = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, neverSyncedCatalogAssetId )
	neverSyncedcatalogDocValue._serverProvidedMetadata = nil

	rxTest:waitFor( wfCatalog:docStore():setDocValue( neverSyncedcatalogDocValue ) )
	
	-- Create an asset in which the latest metadata has not synced to Oz.
	
	local onceSyncedCatalogAssetId = KSIdGenerator.uuidInLowercaseHex()
	local onceSyncedCatalogDocValue = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, onceSyncedCatalogAssetId )
	onceSyncedCatalogDocValue._serverProvidedMetadata = {}
		-- An empty SPM section makes it appear as though it once synced but requires
		-- latest revision to be synced.

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

	-- Delete the assets

	rxTest:waitFor( wfCatalog:docStore():deleteDoc( errorDocId ) )
	rxTest:waitFor( wfCatalog:docStore():deleteDoc( onceSyncedCatalogAssetId ) )
	rxTest:waitFor( wfCatalog:docStore():deleteDoc( neverSyncedCatalogAssetId ) )

	-- Verify only the asset that has previously synced to Oz shows up

	rxTest:expectSingleValue( wfCatalog:indexStore():queryOnce( {
		sql = [[ SELECT assetId FROM unconfirmedDeletions ]],
		quiesceUntilUpToDate = true,
		singleColumnQuery = true
	} ), { onceSyncedCatalogAssetId } )

	stuckSyncObserver:expectDataWithinTime( 5, {} )

	isNormalSyncRunningStream:onNext( false ) -- disable sync

	-- Make it look like the pusher has already processed the deleted documents.

	local catalogPusherLocalDocValueId = WFCatMetadataPusher.getContinueFromLocalDocValueKey( false )

	local catalogPusherLocalDocValue = {
		_id = catalogPusherLocalDocValueId,
		continueFrom = 6
	}

	rxTest:waitFor( wfCatalog:docStore():setLocalDocValue( catalogPusherLocalDocValue ) )

	-- Verify there's no stuck sync because sync is disabled

	stuckSyncObserver:expectDataWithinTime( 5, {} )

	-- Enable sync, and verify the stuck catalog asset

	isNormalSyncRunningStream:onNext( true )

	stuckSyncObserver:expectDataWithinTime( 5, { onceSyncedCatalogAssetId } )

	-- Attempt to resolve the stuck sync

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { onceSyncedCatalogAssetId } ) )

	-- Verify the new revision ID and looper-protection annotation:

	local catalogAssetRevId = rxTest:getFirstValue(
			wfCatalog:docStore():docValue( onceSyncedCatalogAssetId ):pluck( '_revId' ) )

	local revId, optFieldSpec -- nil
	local simplifyDeletedDoc = false

	rxTest:expectSingleValue( wfCatalog:docStore():docValue( onceSyncedCatalogAssetId, revId, optFieldSpec, simplifyDeletedDoc )
			:pluck( '_localOnly', 'stuckSyncUtils', 'lastGeneratedRevisionId' ),
			catalogAssetRevId )

	-- Looper protection should prevent us from generating new revision IDs via resolveStuckSync:

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { onceSyncedCatalogAssetId } ) )

	rxTest:expectSingleValue(
			wfCatalog:docStore():docValue( onceSyncedCatalogAssetId ):pluck( '_revId' ), catalogAssetRevId )

	rxTest:expectSingleValue( wfCatalog:docStore():docValue( onceSyncedCatalogAssetId, revId, optFieldSpec, simplifyDeletedDoc )
			:pluck( '_localOnly', 'stuckSyncUtils', 'lastGeneratedRevisionId' ),
			catalogAssetRevId )

	stuckSyncObserver:expectDataWithinTime( 5, {} )

	stuckSyncObserver:dispose()

	isNormalSyncRunningStream:dispose()

end

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

function WFCatStuckSyncUtilsTest.rxTestMaybeTrackStuckMetadataDocument( rxTest )

	-- This test verifies that we limit the number of analytics messages
	-- we generate for stuck syncs per app session.

	local docValue = {
		_id = KSIdGenerator.uuidInLowercaseHex()
	}
	
	local wasCurrentRevisionGenerated = false
	local generatedRevisionCount = 1
	local isInvalidDocument = false
	local isDeleted = true

	rxTest:expectEqual( true, WFCatStuckSyncUtils._DEBUG_maybeTrackStuckMetadataDocument(
				docValue, wasCurrentRevisionGenerated, generatedRevisionCount, isInvalidDocument, isDeleted ) )

	wasCurrentRevisionGenerated = true

	rxTest:expectEqual( true, WFCatStuckSyncUtils._DEBUG_maybeTrackStuckMetadataDocument(
				docValue, wasCurrentRevisionGenerated, generatedRevisionCount, isInvalidDocument, isDeleted ) )

	rxTest:expectEqual( false, WFCatStuckSyncUtils._DEBUG_maybeTrackStuckMetadataDocument(
				docValue, wasCurrentRevisionGenerated, generatedRevisionCount, isInvalidDocument, isDeleted ) )

end

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

function WFCatStuckSyncUtilsTest.rxTestLooperProtection( rxTest )

	rxTest:setTimeLimit( 5 )

	local wfCatalog = createFakeWFCatalog( rxTest )

	local docStore = wfCatalog:docStore()

	-- Create a catalog asset.

	local docId = KSIdGenerator.uuidInLowercaseHex()

	local docValue = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, docId )

	rxTest:waitFor( docStore:setDocValue( docValue ) )

	-- Call resolveStuckSync using some custom overrides to verify logic

	local overrides = {
		generatedRevisionFrequencyInSeconds = 0.5,
		maxGeneratedRevisionCount = 2
	}

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId }, overrides ) )

	-- Verify the anti-looper annotation

	docValue = rxTest:getFirstValue( docStore:docValue( docId ) )

	local stuckSyncUtilsAnnotation = KSTableUtils.derefOpt( docValue, '_localOnly', 'stuckSyncUtils' )

	local lastGeneratedRevisionId = stuckSyncUtilsAnnotation.lastGeneratedRevisionId
	local lastGeneratedRevisionTimestamp = stuckSyncUtilsAnnotation.lastGeneratedRevisionTimestamp
	local generatedRevisionCount = stuckSyncUtilsAnnotation.generatedRevisionCount

	rxTest:expectEqual( docValue._revId, lastGeneratedRevisionId )
	rxTest:expectEqual( true, lastGeneratedRevisionTimestamp and true ) -- Make sure it exists
	rxTest:expectEqual( 1, generatedRevisionCount )

	-- Looper protection should prevent us from quickly calling resolveStuckSync again

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId }, overrides ) )

	-- Make sure the annotation did *not* change:

	docValue = rxTest:getFirstValue( docStore:docValue( docId ) )

	stuckSyncUtilsAnnotation = KSTableUtils.derefOpt( docValue, '_localOnly', 'stuckSyncUtils' )

	rxTest:expectEqual( lastGeneratedRevisionId, stuckSyncUtilsAnnotation.lastGeneratedRevisionId )
	rxTest:expectEqual( lastGeneratedRevisionTimestamp, stuckSyncUtilsAnnotation.lastGeneratedRevisionTimestamp )
	rxTest:expectEqual( 1, stuckSyncUtilsAnnotation.generatedRevisionCount )

	-- Wait a second.

	rxTest:sleep( overrides.generatedRevisionFrequencyInSeconds )

	-- Now we should be able to call resolveStuckSync and see a new annotation:

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId }, overrides ) )

	docValue = rxTest:getFirstValue( docStore:docValue( docId ) )

	stuckSyncUtilsAnnotation = KSTableUtils.derefOpt( docValue, '_localOnly', 'stuckSyncUtils' )

	rxTest:expectNotEqual( lastGeneratedRevisionId, stuckSyncUtilsAnnotation.lastGeneratedRevisionId )
	rxTest:expectNotEqual( lastGeneratedRevisionTimestamp, stuckSyncUtilsAnnotation.lastGeneratedRevisionTimestamp )
	rxTest:expectEqual( 2, stuckSyncUtilsAnnotation.generatedRevisionCount )

	-- Wait a second.

	rxTest:sleep( overrides.generatedRevisionFrequencyInSeconds )

	-- Now that we've hit the maximum number of generated revisions, resolveStuckSync should be a no-op:

	rxTest:waitFor( WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, { docId }, overrides ) )

	docValue = rxTest:getFirstValue( docStore:docValue( docId ) )

	stuckSyncUtilsAnnotation = KSTableUtils.derefOpt( docValue, '_localOnly', 'stuckSyncUtils' )

	rxTest:expectEqual( 2, stuckSyncUtilsAnnotation.generatedRevisionCount )

end

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

function WFCatStuckSyncUtilsTest.rxTestClearErrorsAndRetrySync( rxTest )

	rxTest:setTimeLimit( 5 )

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

	-- Create an asset with various error states

	local docId = KSIdGenerator.uuidInLowercaseHex()
	local docValue = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, docId )

	-- Add error states to the document

	docValue._localOnly = {
		lastGeneratedRevIdToResolveStuckSync = "old_rev_id",
		lastGeneratedRevIdToResolveStuckSync_new = "old_rev_id_new",
		stuckSyncUtils = { someData = "test" },
		ozError = { error = "test error" },
		localOriginalError = { error = "original error" },
		binaryUploadErrors = { error = "upload error" },
		binaryDownloadErrors = { error = "download error" },
		deniedBy = { user = "test_user" },
		blacklistedBy = { user = "test_user" }
	}

	rxTest:waitFor( docStore:setDocValue( docValue ) )

	-- Call clearErrorsAndRetrySync

	rxTest:waitFor( WFCatStuckSyncUtils.clearErrorsAndRetrySync( wfCatalog, { docId } ) )

	-- Verify the document state after clearing errors

	local updatedDocValue = rxTest:getFirstValue( docStore:docValue( docId ) )

	-- Check that all error states were cleared

	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'lastGeneratedRevIdToResolveStuckSync' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'lastGeneratedRevIdToResolveStuckSync_new' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'stuckSyncUtils' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'ozError' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'localOriginalError' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'binaryUploadErrors' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'binaryDownloadErrors' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'deniedBy' ) )
	rxTest:expectEqual( nil, KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'blacklistedBy' ) )

	-- Check that a new revision ID was generated

	rxTest:expectNotEqual( docValue._revId, updatedDocValue._revId )

	-- Check that history was updated with the clear operation

	local history = KSTableUtils.derefOpt( updatedDocValue, '_localOnly', 'history' ) or {}

	rxTest:expectEqual( true, not not next( history ) )

end

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

return WFRxTestCase.wrapSuite( 'WFCatStuckSyncUtilsTest', WFCatStuckSyncUtilsTest )
