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

LrDocStoreTypeMigratorTest.lua

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

Copyright 2020 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.

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSPathUtils = require 'KSPathUtils'

-- local WFCloseSupport = require 'WFCloseSupport'
local WFFileUtils = require 'WFFileUtils'

	-- WF.sqlite.database
local WFSQLiteDatabase = require "WFSQLiteDatabase"
local WFSQLiteDatabaseSchema = require "WFSQLiteDatabaseSchema"

	-- WF.oz_client.doc_store
local LrDocStoreTypeMigrator = require 'LrDocStoreTypeMigrator'
local WFRxDocStore = require 'WFRxDocStore'

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

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

local LrDocStoreTypeMigratorTest = {}

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

local function selectFirst( a, _ )

	return a

end

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

local function restorePreTypeMigrationDocStore( docStorePath )

	-- brute force the removal of columns 'type' and 'subtype' to simulate
	-- a pre type migration style docStore.

	local db = WFSQLiteDatabase( docStorePath, WFSQLiteDatabaseSchema( "1.2" ) )
	-- switch off foreign keys
	db:execOnce( [[PRAGMA foreign_keys = OFF]] )
	-- create and copy old style docs table
	db:execOnce( [[CREATE TABLE
					new_docs( localDocId INTEGER PRIMARY KEY,
						 fullDocId TEXT UNIQUE NOT NULL,
						 winningRevSequence INTEGER REFERENCES revs( sequence ) ON DELETE SET NULL,
						 mostRecentRevSequence INTEGER REFERENCES revs( sequence ) ON DELETE SET NULL,
						 deleted BOOLEAN DEFAULT 0,
						 hasConflicts BOOLEAN DEFAULT 0,
						 annotation BLOB,
						 spaceId TEXT )]] )
	db:execOnce( [[INSERT INTO new_docs (localDocId, fullDocId, winningRevSequence, mostRecentRevSequence,
										deleted, hasConflicts, annotation, spaceId)
										SELECT localDocId, fullDocId, winningRevSequence, mostRecentRevSequence,
										deleted, hasConflicts, annotation, spaceId FROM docs]] )
	db:execOnce( [[DROP TABLE docs]] )
	db:execOnce( [[ALTER TABLE new_docs RENAME TO docs]] )
	-- recreate indices
	db:execOnce( [[CREATE INDEX docs_mostRecentRevSequence ON docs( mostRecentRevSequence, spaceId )]] )
	db:execOnce( [[CREATE INDEX docs_winningRevSequence ON docs( winningRevSequence )]] )
	db:execOnce( [[CREATE INDEX docs_fullDocId ON docs( fullDocId )]] )
	db:execOnce( [[CREATE INDEX docs_spaceId ON docs( spaceId ) WHERE spaceId NOT NULL]] )
	-- restore old schema version
	db:execOnce( [[UPDATE schemaVersion SET currentVersion ="1.1" WHERE currentVersion = "1.2"]] )
	-- switch on foreign keys
	db:execOnce( [[PRAGMA foreign_keys = OFF]] )

	db:close()
	db = nil

end

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

function LrDocStoreTypeMigratorTest.rxTestBackFillTypeAndSubtype( rxTest )

	rxTest:setTimeLimit( 5 )

	local emptyAssetId  = KSIdGenerator.uuidInLowercaseHex()
	local assetId 		= KSIdGenerator.uuidInLowercaseHex()
	local folderId 		= KSIdGenerator.uuidInLowercaseHex()
	local albumId 		= KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId 	= KSIdGenerator.uuidInLowercaseHex()

	local numDocsNeedBackFillTypeAndSubtype = LrDocStoreTypeMigrator.needBackFillTypeAndSubtype( "NotACatalogDocstore.db" )
	rxTest:expectEqual( 0, numDocsNeedBackFillTypeAndSubtype )
		-- User doc store should not need back fill.

	local docStorePath = KSPathUtils.getStandardFilePath( "temp" )
	docStorePath = KSPathUtils.child( docStorePath, 'rxTestBackFillTypeAndSubtype-Managed Catalog.mcat' )
	docStorePath = WFFileUtils.ensureFileNameIsUnique( docStorePath )
	
	numDocsNeedBackFillTypeAndSubtype = LrDocStoreTypeMigrator.needBackFillTypeAndSubtype( docStorePath )
	rxTest:expectEqual( 0, numDocsNeedBackFillTypeAndSubtype )
	
	local docStore = WFRxDocStore( docStorePath )

		-- cleanup after the test
	rxTest:finally( function()
		if docStorePath then
			WFFileUtils.delete( docStorePath )
			WFFileUtils.delete( docStorePath .. '-shm' )
			WFFileUtils.delete( docStorePath .. '-wal' )
		end
	end)

	-- fill in some docs
	rxTest:waitFor(
		docStore:bulkSetDocValue {
			-- assetWithEmptyContent
			{
				_id = emptyAssetId,
				_localOnly = {
					favoritesSlurpDone = true,
				},
			},
			-- asset
			{
				_id = assetId,
				captureDate = "2015-05-17T15:50:12",
				importSource = {
					importTimestamp = "2015-05-26T17:34:23-07:00",
					originalHeight = 1600,
					originalWidth = 1068,
				},
				location = {
					altitude = 377.9,
					latitude = 45.951733,
					longitude = -93.600107,
				},
				subtype = "image",
				type = "asset",
				version = 1,
				xmp = {
					dc = {
						description = "Rum River State Forest, near Onamia, Minnesota",
						rights = "test copyright string",
					},
				},
			},
			-- album_asset
			{
				_id = albumAssetId,
				album = {
					id = albumId,
				},
				asset = {
					id = assetId,
				},
				cover = true,
				order = "V",
				type = "album_asset",
			},
			-- album (folder)
			{
				_id = folderId,
				name = "My folder",
				type = "album",
				subtype = "collection_set",
			},
			-- album (collection)
			{
				_id = albumId,
				cover = {
					id = assetId,
				},
				parent = folderId,
				name = "Sync me",
				type = "album",
				subtype = "collection",
			},
		} )

	-- bring docStore to halt
	rxTest:expectOnlyCompleted( docStore:halt() )
	docStore.close()
	docStore = nil

	restorePreTypeMigrationDocStore( docStorePath )

	numDocsNeedBackFillTypeAndSubtype = LrDocStoreTypeMigrator.needBackFillTypeAndSubtype( docStorePath )
	rxTest:expectEqual( 5, numDocsNeedBackFillTypeAndSubtype )

	local docStoreInitProgressModel = require( 'LrDocStoreTypeMigratorProgressModel' ).makeModel( KSPathUtils.leafName( docStorePath ) )

	-- We only receive a progress tick every 1000 documents, so for this tiny db, we just get completed
	local progress = rxTest:observe( docStoreInitProgressModel.value )
		
	rxTest:finally( function()
		progress:dispose()
	end )

	docStore = WFRxDocStore( docStorePath )

	rxTest:waitFor( docStore
					:docValue( assetId )
					:take( 1 ) )


	progress:expectCompletedWithinTime( 2 )

	local docRows = docStore:getDocsOfType( { asset = { 'image' } } )
		:map( selectFirst )

	local obs = rxTest:observe( docRows )

	obs:expectData( {
		assetId,
	} )

	docRows = docStore:getDocsOfType( { album = { '*' } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {
		folderId,
		albumId,
	} )

	docRows = docStore:getDocsOfType( { album_asset = { "NULL" } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {
		albumAssetId,
	} )

		-- halt the docStore
	rxTest:expectOnlyCompleted( docStore:halt() )
	docStore.close()
	docStore = nil
	
	numDocsNeedBackFillTypeAndSubtype = LrDocStoreTypeMigrator.needBackFillTypeAndSubtype( docStorePath )
	rxTest:expectEqual( 0, numDocsNeedBackFillTypeAndSubtype )

end

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

function LrDocStoreTypeMigratorTest.rxTestUnknownDocTypes( rxTest )

	rxTest:setTimeLimit( 5 )

	local assetId 			= KSIdGenerator.uuidInLowercaseHex()
	local newDocTypeId1 	= KSIdGenerator.uuidInLowercaseHex()
	local newDocTypeId2 	= KSIdGenerator.uuidInLowercaseHex()
	local brokenDocTypeId1 	= KSIdGenerator.uuidInLowercaseHex()
	local brokenDocTypeId2 	= KSIdGenerator.uuidInLowercaseHex()
	local brokenDocTypeId3 	= KSIdGenerator.uuidInLowercaseHex()
	local brokenDocTypeId4 	= KSIdGenerator.uuidInLowercaseHex()


	local docStorePath = KSPathUtils.getStandardFilePath( "temp" )
	docStorePath = KSPathUtils.child( docStorePath, 'rxTestUnknownDocTypes-Managed Catalog.mcat' )
	docStorePath = WFFileUtils.ensureFileNameIsUnique( docStorePath )
	local docStore = WFRxDocStore( docStorePath )

		-- cleanup after the test
	rxTest:finally( function()
		if docStorePath then
			WFFileUtils.delete( docStorePath )
			WFFileUtils.delete( docStorePath .. '-shm' )
			WFFileUtils.delete( docStorePath .. '-wal' )
		end
	end)


	local type1 = "newDocType"				-- newDocType
	local subtype1 = "newDocSubtype"
	local type2 = "newDocType1-X"			-- newDocTypes with hyphen, digits and underscores
	local subtype2 = "newDocSubtype2_Y"
	local brokenType1 = "1newDocType"		-- type must begin with alphabetic char
	local brokenType2 = "newDoc Type"		-- no spaces please
	local brokenType3 = "newDoc.Type"		-- only alnum chars
	local brokenType4 = "ThisIsWayToLongATypeShouldOnlyBeFiftyCharactersMaximum" -- not more than 50 chars long
	local brokenSubtype = "newDocSubtype"


	-- fill in some docs
	rxTest:waitFor(
		docStore:bulkSetDocValue {
			-- asset
			{
				_id = assetId,
				captureDate = "2015-05-17T15:50:12",
				importSource = {
					importTimestamp = "2015-05-26T17:34:23-07:00",
					originalHeight = 1600,
					originalWidth = 1068,
				},
				location = {
					altitude = 377.9,
					latitude = 45.951733,
					longitude = -93.600107,
				},
				subtype = "image",
				type = "asset",
				version = 1,
				xmp = {
					dc = {
						description = "Rum River State Forest, near Onamia, Minnesota",
						rights = "test copyright string",
					},
				},
			},

			{
				_id = newDocTypeId1,
				type = type1,
				subtype = subtype1,
			},

			{
				_id = newDocTypeId2,
				type = type2,
				subtype = subtype2,
			},
			{
				_id = brokenDocTypeId1,
				type = brokenType1,
				subtype = brokenSubtype,
			},
			{
				_id = brokenDocTypeId2,
				type = brokenType2,
				subtype = brokenSubtype,
			},
			{
				_id = brokenDocTypeId3,
				type = brokenType3,
				subtype = brokenSubtype,
			},
			{
				_id = brokenDocTypeId4,
				type = brokenType4,
				subtype = brokenSubtype,
			},
		} )

	-- bring docStore to halt
	rxTest:expectOnlyCompleted( docStore:halt() )
	docStore.close()
	docStore = nil

	restorePreTypeMigrationDocStore( docStorePath )

	docStore = WFRxDocStore( docStorePath )

	rxTest:waitFor( docStore
					:docValue( assetId )
					:take( 1 ) )


	local docRows = docStore:getDocsOfType( { [ type1 ] = { subtype1 } } )
		:map( selectFirst )

	local obs = rxTest:observe( docRows )

	obs:expectData( {
		newDocTypeId1,
	} )

	docRows = docStore:getDocsOfType( { [ type2 ] = { subtype2 } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {
		newDocTypeId2,
	} )

	docRows = docStore:getDocsOfType( { [ brokenType1 ] = { brokenSubtype } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {} )

	docRows = docStore:getDocsOfType( { [ brokenType2 ] = { brokenSubtype } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {} )

	docRows = docStore:getDocsOfType( { [ brokenType3 ] = { brokenSubtype } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {} )

	docRows = docStore:getDocsOfType( { [ brokenType4 ] = { brokenSubtype } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {} )

	-- halt the docStore
	rxTest:expectOnlyCompleted( docStore:halt() )
	docStore.close()
	docStore = nil

end

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

function LrDocStoreTypeMigratorTest.rxTestAddNewDocsToCatalog( rxTest )

	rxTest:setTimeLimit( 2 )

	local assetId 		= KSIdGenerator.uuidInLowercaseHex()
	local folderId 		= KSIdGenerator.uuidInLowercaseHex()
	local albumId 		= KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId 	= KSIdGenerator.uuidInLowercaseHex()

	local docStore = WFRxDocStore( ":memory:" )

	-- add some docs, is the type and subtype correctly added?
	rxTest:waitFor(
		docStore:bulkSetDocValue {
			-- asset
			{
				_id = assetId,
				captureDate = "2015-05-17T15:50:12",
				importSource = {
					importTimestamp = "2015-05-26T17:34:23-07:00",
					originalHeight = 1600,
					originalWidth = 1068,
				},
				location = {
					altitude = 377.9,
					latitude = 45.951733,
					longitude = -93.600107,
				},
				subtype = "image",
				type = "asset",
				version = 1,
				xmp = {
					dc = {
						description = "Rum River State Forest, near Onamia, Minnesota",
						rights = "test copyright string",
					},
				},
			},
			-- album (folder)
			{
				_id = folderId,
				name = "My folder",
				type = "album",
				subtype = "collection_set",
			},
			-- album (collection)
			{
				_id = albumId,
				cover = {
					id = assetId,
				},
				parent = folderId,
				name = "Sync me",
				type = "album",
				subtype = "collection",
			},

			-- album_asset
			{
				_id = albumAssetId,
				album = {
					id = albumId,
				},
				asset = {
					id = assetId,
				},
				cover = true,
				order = "V",
				type = "album_asset",
			},
		} )

	-- add one more new doc, but with the regular setDocValue API
	local assetId2 = KSIdGenerator.uuidInLowercaseHex()

	rxTest:waitFor(
		docStore:setDocValue(
		{
			_id = assetId2,
			captureDate = "2015-05-17T15:50:12",
			importSource = {
				importTimestamp = "2015-05-26T17:34:23-07:00",
				originalHeight = 1600,
				originalWidth = 1068,
			},
			location = {
				altitude = 377.9,
				latitude = 45.951733,
				longitude = -93.600107,
			},
			subtype = "image",
			type = "asset",
			version = 1,
			xmp = {
				dc = {
					description = "Rum River State Forest, near Onamia, Minnesota",
					rights = "test copyright string",
				},
			},
		}) )

	local docRows = docStore:getDocsOfType( { asset = { "image" } } )
		:map( function( docIds, seq )
			return docIds
		end )

	local obs = rxTest:observe( docRows )

	obs:expectData( {
		assetId,
		assetId2,
	} )

	docRows = docStore:getDocsOfType( { album_asset = { "NULL" } } )
		:map( function( docIds, seq )
			return docIds
		end )
	obs = rxTest:observe( docRows )

	obs:expectData( {
		albumAssetId,
	} )

	-- now apply an overlay to an existing doc, type and subtype still correct?
	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId,
			version = 2
		})

	docRows = docStore:getDocsOfType( { asset = { "image" } } )
		:map( function( docIds, seq )
			return docIds
		end )
		
	obs = rxTest:observe( docRows )

	obs:expectData( {
		assetId2, -- Oldest document, in terms of change counter, should come first.
		assetId
	} )

	-- create a new doc without type/subtype and introduce type/subtype via applyOverlay
	local assetId3 = KSIdGenerator.uuidInLowercaseHex()

	rxTest:waitFor(
		docStore:setDocValue( {
		_id = assetId3,
		version = 1,
	}) )

	rxTest:waitFor(
		docStore:applyOverlay {
			_id = assetId3,
			captureDate = "2015-05-17T15:50:12",
			importSource = {
				importTimestamp = "2015-05-26T17:34:23-07:00",
				originalHeight = 1600,
				originalWidth = 1068,
			},
			location = {
				altitude = 377.9,
				latitude = 45.951733,
				longitude = -93.600107,
			},
			subtype = "image",
			type = "asset",
			version = 2,
			xmp = {
			dc = {
				description = "Rum River State Forest, near Onamia, Minnesota",
				rights = "test copyright string",
			},
		}
	})

	docRows = docStore:getDocsOfType( { asset = { "image" } } )
		:map( selectFirst )

	obs = rxTest:observe( docRows )

	obs:expectData( {
		assetId2,
		assetId,
		assetId3,
	} )

	-- let's add a video to our catalog

	local assetId4 = KSIdGenerator.uuidInLowercaseHex()

	rxTest:waitFor(
		docStore:setDocValue(
		{
			_id = assetId4,
			subtype = "video",
			type = "asset",
			version = 1,
		}) )

	-- finally, query for all docIds for all the types and subtypes we've added to the catalog

	docRows = docStore:getDocsOfType( {
			asset = { "image", "video" },
			album = { "*" },
			album_asset = { "NULL" },
		} )
		:map( selectFirst )	

	obs = rxTest:observe( docRows )

	obs:expectData( {
		folderId,
		albumId,
		albumAssetId,
		assetId2,
		assetId,
		assetId3,
		assetId4,
	} )

	-- halt the docStore
	rxTest:expectOnlyCompleted( docStore:halt() )
	docStore.close()
	docStore = nil

end

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

return WFRxTestCase.wrapSuite( 'LrDocStoreTypeMigratorTest', LrDocStoreTypeMigratorTest )
