import { ModelType } from '../../BaseModel';
import { UserData } from '../../services/database/types';
import { msleep, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import Folder from '../Folder';
import Note from '../Note';
import Resource from '../Resource';
import Tag from '../Tag';
import { LoadOptions } from './types';
import { deleteItemUserData, deleteNoteUserData, getItemUserData, getNoteUserData, mergeUserData, setItemUserData, setNoteUserData } from './userData';

const loadOptions: LoadOptions = { fields: ['id', 'parent_id', 'user_data', 'updated_time'] };

describe('utils/userData', () => {

	beforeEach(async () => {
		await setupDatabaseAndSynchronizer(1);
		await switchClient(1);
	});

	it('should set and get user data on a note', async () => {
		const folder = await Folder.save({});
		let note = await Note.save({ parent_id: folder.id });
		note = await Note.load(note.id, loadOptions);
		await msleep(5);
		await setNoteUserData(note, 'org.joplin', 'my-key', 'something');

		const noteReloaded = await Note.load(note.id);
		expect(await getNoteUserData(noteReloaded, 'org.joplin', 'my-key')).toBe('something');

		// Check that the updated_time has been updated (for sync purposes), but
		// not the user_updated_time.
		expect(noteReloaded.updated_time).toBeGreaterThan(note.updated_time);
		expect(noteReloaded.user_updated_time).toBe(note.updated_time);

		// Check for non-existing props
		expect(await getNoteUserData(noteReloaded, 'org.doesntexist', 'my-key')).toBe(undefined);
		expect(await getNoteUserData(noteReloaded, 'org.joplin', 'doesntexist')).toBe(undefined);
	});

	it('should set and get user data on any item', async () => {
		const folder = await Folder.save({});
		const tag = await Tag.save({});
		const resource = await Resource.save({ mime: 'plain/text' });

		await setItemUserData(ModelType.Folder, folder.id, 'foldertest', 'folderkey', 123);
		expect(await getItemUserData(ModelType.Folder, folder.id, 'foldertest', 'folderkey')).toBe(123);
		await deleteItemUserData(ModelType.Folder, folder.id, 'foldertest', 'folderkey');
		expect(await getItemUserData(ModelType.Folder, folder.id, 'foldertest', 'folderkey')).toBe(undefined);

		await setItemUserData(ModelType.Tag, tag.id, 'tagtest', 'tagkey', 123);
		expect(await getItemUserData(ModelType.Tag, tag.id, 'tagtest', 'tagkey')).toBe(123);
		await deleteItemUserData(ModelType.Tag, tag.id, 'tagtest', 'tagkey');
		expect(await getItemUserData(ModelType.Tag, tag.id, 'tagtest', 'tagkey')).toBe(undefined);

		await setItemUserData(ModelType.Resource, resource.id, 'resourcetest', 'resourcekey', 123);
		expect(await getItemUserData(ModelType.Resource, resource.id, 'resourcetest', 'resourcekey')).toBe(123);
		await deleteItemUserData(ModelType.Resource, resource.id, 'resourcetest', 'resourcekey');
		expect(await getItemUserData(ModelType.Resource, resource.id, 'resourcetest', 'resourcekey')).toBe(undefined);
	});

	it('should delete user data', async () => {
		const folder = await Folder.save({});
		let note = await Note.save({ parent_id: folder.id });
		note = await Note.load(note.id, loadOptions);
		await setNoteUserData(note, 'org.joplin', 'my-key', 'something');

		let noteReloaded = await Note.load(note.id);
		expect(await getNoteUserData(noteReloaded, 'org.joplin', 'my-key')).toBe('something');

		noteReloaded = await deleteNoteUserData(noteReloaded, 'org.joplin', 'my-key');
		expect(await getNoteUserData(noteReloaded, 'org.joplin', 'my-key')).toBe(undefined);

		// Check that it works if we set it again
		await setNoteUserData(note, 'org.joplin', 'my-key', 'something else');
		noteReloaded = await Note.load(noteReloaded.id, loadOptions);
		expect(await getNoteUserData(noteReloaded, 'org.joplin', 'my-key')).toBe('something else');
	});

	it('should merge user data', async () => {
		const testCases: [UserData, UserData, UserData][] = [
			[
				{
					'org.joplin': {
						'k1': {
							v: 123,
							t: 0,
						},
						'k3': {
							v: 789,
							t: 5,
						},
						'k4': {
							v: 789,
							t: 5,
						},
					},
					'com.example': {},
				},
				{
					'org.joplin': {
						'k1': {
							v: 456,
							t: 1,
						},
						'k2': {
							v: 'abc',
							t: 5,
						},
						'k4': {
							v: 111,
							t: 0,
						},
					},
				},
				{
					'org.joplin': {
						'k1': {
							v: 456,
							t: 1,
						},
						'k2': {
							v: 'abc',
							t: 5,
						},
						'k3': {
							v: 789,
							t: 5,
						},
						'k4': {
							v: 789,
							t: 5,
						},
					},
					'com.example': {},
				},
			],

			[
				// Client 2 delete a prop
				// Later, client 1 update that prop
				// Then data is merged
				// => In that case, the data is restored using client 1 data
				{
					'org.joplin': {
						'k1': {
							v: 123,
							t: 10,
						},
					},
				},
				{
					'org.joplin': {
						'k1': {
							v: 0,
							t: 0,
							d: 1,
						},
					},
				},
				{
					'org.joplin': {
						'k1': {
							v: 123,
							t: 10,
						},
					},
				},
			],

			[
				// Client 1 update a prop
				// Later, client 2 delete a prop
				// Then data is merged
				// => In that case, the data is deleted and the update from client 1 is lost
				{
					'org.joplin': {
						'k1': {
							v: 123,
							t: 0,
						},
					},
				},
				{
					'org.joplin': {
						'k1': {
							v: 0,
							t: 10,
							d: 1,
						},
					},
				},
				{
					'org.joplin': {
						'k1': {
							v: 0,
							t: 10,
							d: 1,
						},
					},
				},
			],
		];

		for (const [target, source, expected] of testCases) {
			const actual = mergeUserData(target, source);
			expect(actual).toEqual(expected);
		}
	});

});
