import { ChangeType, Session, Share, ShareType, ShareUser, ShareUserStatus } from '../../services/database/types';
import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession, models, createNote, createFolder, updateItem, createItemTree, updateNote, expectHttpError, createResource, expectNotThrow, updateFolder, db } from '../../utils/testing/testUtils';
import { postApi, patchApi, getApi, deleteApi } from '../../utils/testing/apiUtils';
import { PaginatedDeltaChanges } from '../../models/ChangeModel/ChangeModel';
import { inviteUserToShare, shareFolderWithUser } from '../../utils/testing/shareApiUtils';
import { msleep } from '../../utils/time';
import { ErrorForbidden } from '../../utils/errors';
import { resourceBlobPath, serializeJoplinItem, unserializeJoplinItem } from '../../utils/joplinUtils';
import { PaginatedItems } from '../../models/ItemModel';
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
import { makeNoteSerializedBody } from '../../utils/testing/serializedItems';

const createSingleFolderShare = async (session1: Session, session2: Session) => {
	const folderItem = await createFolder(session1.id, { title: 'created by sharer' });

	// ----------------------------------------------------------------
	// Create the file share object
	// ----------------------------------------------------------------
	const share = await postApi<Share>(session1.id, 'shares', {
		type: ShareType.Folder,
		folder_id: folderItem.jop_id,
	});

	// ----------------------------------------------------------------
	// Once the share object has been created, the client can add folders
	// and notes to it. This is done by setting the share_id property,
	// which we simulate here.
	// ----------------------------------------------------------------
	await models().item().saveForUser(session1.user_id, {
		id: folderItem.id,
		jop_parent_id: '',
		jop_share_id: share.id,
	});

	// ----------------------------------------------------------------
	// Send the share to a user
	// ----------------------------------------------------------------
	const user2 = await models().user().load(session2.user_id);
	let shareUser = await postApi(session1.id, `shares/${share.id}/users`, {
		email: user2.email,
	}) as ShareUser;

	shareUser = await models().shareUser().load(shareUser.id);
	expect(shareUser.share_id).toBe(share.id);
	expect(shareUser.user_id).toBe(user2.id);
	expect(shareUser.status).toBe(ShareUserStatus.Waiting);

	// ----------------------------------------------------------------
	// On the sharee side, accept the share
	// ----------------------------------------------------------------
	await patchApi<ShareUser>(session2.id, `share_users/${shareUser.id}`, { status: ShareUserStatus.Accepted });

	{
		shareUser = await models().shareUser().load(shareUser.id);
		expect(shareUser.status).toBe(ShareUserStatus.Accepted);
	}

	await models().share().updateSharedItems3();

	// ----------------------------------------------------------------
	// On the sharee side, check that the file is present
	// and with the right content.
	// ----------------------------------------------------------------
	const results = await getApi<PaginatedItems>(session2.id, 'items/root/children');
	expect(results.items.length).toBe(1);
	expect(results.items[0].name).toBe(folderItem.name);

	const itemContent = await getApi<Buffer>(session2.id, `items/root:/${folderItem.name}:/content`);
	expect(itemContent.toString().includes('created by sharer')).toBe(true);

	return { folderItem, itemContent, share };
};

describe('shares.folder', () => {

	beforeAll(async () => {
		await beforeAllDb('shares.folder');
	});

	afterAll(async () => {
		await afterAllTests();
	});

	beforeEach(async () => {
		await beforeEachDb();
	});

	test('should share a folder with another user', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);
		await createSingleFolderShare(session1, session2);
	});

	test('sharer should see change if item is changed by sharee', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);
		const { folderItem, itemContent } = await createSingleFolderShare(session1, session2);

		// ----------------------------------------------------------------
		// If file is changed by sharee, sharer should see the change too
		// ----------------------------------------------------------------
		{
			const folder = await unserializeJoplinItem(itemContent.toString());
			folder.title = 'modified by recipient';
			await updateItem(session2.id, `root:/${folderItem.name}:`, await serializeJoplinItem(folder));

			// Double check that it didn't create a new item instead of updating it
			expect((await models().item().all()).length).toBe(1);

			const modContent = await getApi<Buffer>(session1.id, `items/root:/${folderItem.name}:/content`);
			expect(modContent.toString().includes('modified by recipient')).toBe(true);
		}
	});

	test('should not delete an item on unshare', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);
		const { folderItem, share } = await createSingleFolderShare(session1, session2);

		await models().item().saveForUser(session2.user_id, {
			id: folderItem.id,
			jop_parent_id: 'changed',
			jop_share_id: share.id,
		});

		await models().item().saveForUser(session1.user_id, {
			id: folderItem.id,
			jop_parent_id: 'changed',
			jop_share_id: '',
		});

		await models().share().updateSharedItems3();

		const user1HasAccess = !!await models().userItem().byUserAndItemId(session1.user_id, folderItem.id);
		const user2HasAccess = !!await models().userItem().byUserAndItemId(session2.user_id, folderItem.id);
		expect({ user1HasAccess, user2HasAccess }).toEqual({ user1HasAccess: true, user2HasAccess: false });
	});

	test('should share a folder and all its children', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F3', [
			{
				id: '000000000000000000000000000000F1',
				children: [],
			},
			{
				id: '000000000000000000000000000000F2',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
			{
				id: '000000000000000000000000000000F3',
				children: [
					{
						id: '00000000000000000000000000000003',
					},
					{
						id: '000000000000000000000000000000F4',
						children: [
							{
								id: '00000000000000000000000000000004',
							},
							{
								id: '00000000000000000000000000000005',
							},
						],
					},
				],
			},
		]);

		const children1 = await getApi<PaginatedItems>(session1.id, 'items/root/children');
		expect(children1.items.length).toBe(8);

		const children2 = await getApi<PaginatedItems>(session2.id, 'items/root/children');
		expect(children2.items.length).toBe(5);

		const expectedNames = [
			'000000000000000000000000000000F3.md',
			'00000000000000000000000000000003.md',
			'000000000000000000000000000000F4.md',
			'00000000000000000000000000000004.md',
			'00000000000000000000000000000005.md',
		];

		expect(children2.items.map(i => i.name).sort().join(',')).toBe(expectedNames.sort().join(','));
	});

	test('should received shared items only once invitation accepted', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { shareUser } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		], false);

		// The invitation has not been accepted yet, so user 2 should not see any item

		{
			const children2 = await getApi<PaginatedItems>(session2.id, 'items/root/children');
			expect(children2.items.length).toBe(0);
		}

		await patchApi(session2.id, `share_users/${shareUser.id}`, { status: ShareUserStatus.Accepted });

		// As soon as the invitation is accepted, all items should be available,
		// without having to wait for the share service.

		{
			const children2 = await getApi<PaginatedItems>(session2.id, 'items/root/children');
			expect(children2.items.length).toBe(2);
		}
	});

	test('should share when a note is added to a shared folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const { share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F2', [
			{
				id: '000000000000000000000000000000F2',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		]);

		await createNote(session1.id, {
			id: '00000000000000000000000000000002',
			parent_id: '000000000000000000000000000000F2',
			share_id: share.id,
		});

		await models().share().updateSharedItems3();

		{
			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(3);
			expect(!!newChildren.items.find(i => i.name === '00000000000000000000000000000002.md')).toBe(true);
		}

		await createNote(session2.id, {
			id: '00000000000000000000000000000003',
			parent_id: '000000000000000000000000000000F2',
			share_id: share.id,
		});

		await models().share().updateSharedItems3();

		{
			const newChildren = await models().item().children(user1.id);
			expect(newChildren.items.length).toBe(4);
			expect(!!newChildren.items.find(i => i.name === '00000000000000000000000000000003.md')).toBe(true);
		}
	});

	test('should allow recipient to modify the root folder title', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { item: rootFolderItem } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F2', [
			{
				id: '000000000000000000000000000000F2',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		]);

		await updateFolder(session2.id, {
			id: '000000000000000000000000000000F2',
			title: 'update from session 2',
		});

		const rootFolder = await models().item().loadAsJoplinItem<FolderEntity>(rootFolderItem.id);
		expect(rootFolder.title).toBe('update from session 2');
	});

	test('should update share status when note parent changes', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const { share: share1 } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
					{
						id: '000000000000000000000000000000F2',
						children: [],
					},
				],
			},
		]);

		const { share: share2 } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F5', [
			{
				id: '000000000000000000000000000000F3',
				children: [],
			},
			{
				id: '000000000000000000000000000000F4',
				children: [],
			},
			{
				id: '000000000000000000000000000000F5',
				children: [],
			},
		]);

		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');

		// Note is moved to another folder, but still within shared folder

		{
			await models().item().saveForUser(user1.id, {
				id: noteItem.id,
				jop_parent_id: '000000000000000000000000000000F2',
				jop_share_id: share1.id,
			});

			await models().share().updateSharedItems3();

			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(4);
		}

		// Note is moved to another folder, outside to a non-shared folder

		{
			await models().item().saveForUser(user1.id, {
				id: noteItem.id,
				jop_parent_id: '000000000000000000000000000000F3',
				jop_share_id: '',
			});

			await models().share().updateSharedItems3();

			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(3);
			expect(newChildren.items.find(i => i.name === '00000000000000000000000000000001.md')).toBe(undefined);
		}

		// Note is moved from a non-shared folder to another non-shared folder

		{
			await models().item().saveForUser(user1.id, {
				id: noteItem.id,
				jop_parent_id: '000000000000000000000000000000F4',
				jop_share_id: '',
			});

			await models().share().updateSharedItems3();

			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(3);
			expect(newChildren.items.find(i => i.name === '00000000000000000000000000000001.md')).toBe(undefined);
		}

		// Note is moved from a non-shared folder, back to a shared folder

		{
			await models().item().saveForUser(user1.id, {
				id: noteItem.id,
				jop_parent_id: '000000000000000000000000000000F1',
				jop_share_id: share1.id,
			});

			await models().share().updateSharedItems3();

			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(4);
			expect(!!newChildren.items.find(i => i.name === '00000000000000000000000000000001.md')).toBe(true);
		}

		// Note is moved from a shared folder to a different shared folder

		{
			await models().item().saveForUser(user1.id, {
				id: noteItem.id,
				jop_parent_id: '000000000000000000000000000000F5',
				jop_share_id: share2.id,
			});

			await models().share().updateSharedItems3();

			const newChildren = await models().item().children(user2.id);
			expect(newChildren.items.length).toBe(4);
			expect(!!newChildren.items.find(i => i.name === '00000000000000000000000000000001.md')).toBe(true);
		}
	});

	test('should update share status when note parent changes more than once between updates', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
			'000000000000000000000000000000F2': {},
			'000000000000000000000000000000F3': {},
		});

		const folderItem1 = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');

		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');

		// Note is moved to a non-shared folder and changed twice, but the
		// parent ID doesn't change.

		await models().item().saveForUser(user1.id, {
			id: noteItem.id,
			jop_parent_id: '000000000000000000000000000000F2',
			jop_share_id: '',
		});

		await models().item().saveForUser(user1.id, {
			id: noteItem.id,
			jop_parent_id: '000000000000000000000000000000F2',
			jop_share_id: '',
		});

		await models().share().updateSharedItems3();

		const newChildren = await models().item().children(user2.id);
		expect(newChildren.items.length).toBe(1);
		expect(newChildren.items[0].id).toBe(folderItem1.id);
	});

	test('should not throw an error if an item is associated with a share that no longer exists', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {},
		});

		await createNote(session1.id, {
			id: '00000000000000000000000000000007',
			share_id: share.id,
		});

		await models().share().delete(share.id);

		await expectNotThrow(async () => await models().share().updateSharedItems3());
	});

	test('should not throw an error if a change is associated with an item that no longer exists', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { share: share1 } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {},
		});

		const { share: share2 } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F2', {
			'000000000000000000000000000000F2': {},
		});

		const item1 = await createNote(session1.id, {
			id: '00000000000000000000000000000007',
			share_id: share1.id,
		});

		await createNote(session1.id, {
			id: '00000000000000000000000000000008',
			share_id: share2.id,
		});

		await models().item().delete(item1.id);

		await models().share().updateSharedItems3();

		await expectNotThrow(async () => await models().share().updateSharedItems3());
	});

	test('should not throw an error if a user no longer has a user item, and the target share changes', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		]);

		const { share: share2 } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F5', [
			{
				id: '000000000000000000000000000000F5',
				children: [],
			},
		]);

		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');

		// Note is moved from a shared folder to a different shared folder

		await models().item().saveForUser(user1.id, {
			id: noteItem.id,
			jop_parent_id: '000000000000000000000000000000F5',
			jop_share_id: share2.id,
		});

		await models().userItem().deleteByUserItem(user2.id, noteItem.id, { recordChanges: false });

		await expectNotThrow(async () => await models().share().updateSharedItems3());
	});

	test('should unshare a deleted item', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		expect((await models().item().children(user2.id)).items.length).toBe(2);

		const noteModel = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');

		await models().item().delete(noteModel.id);

		expect((await models().item().children(user2.id)).items.length).toBe(1);
	});

	test('should unshare a deleted shared root folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		const folderItem = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');

		await models().item().delete(folderItem.id);

		// await models().share().updateSharedItems3();

		// Once the root folder has been deleted, it is unshared, so the
		// recipient user should no longer see any item
		expect((await models().item().children(user2.id)).items.length).toBe(0);

		// Even though the root folder has been deleted, its children have not
		// been (they are deleted by the client), so the owner should still see
		// one child.
		expect((await models().item().children(user1.id)).items.length).toBe(1);

		// Also check that Share and UserShare objects are deleted
		expect((await models().share().all()).length).toBe(0);
		expect((await models().shareUser().all()).length).toBe(0);

		// Test deleting the share, but not the root folder
	});

	test.each([
		{
			label: 'not delete a shared root folder when deleted by the recipient',
			deleteFromOwner: false,
		},
		{
			label: 'delete a shared root folder when deleted by the owner',
			deleteFromOwner: true,
		},
	])('should $label', async ({ deleteFromOwner }) => {
		const { user: owner, session: session1 } = await createUserAndSession(1);
		const { user: recipient, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '0000000TEST0FOLDER01000000000000', {
			'0000000TEST0FOLDER01000000000000': { },
		});

		const folderItem = await models().item().loadByJopId(owner.id, '0000000TEST0FOLDER01000000000000');

		const itemIdsForUser = async (userId: string) => {
			return (await models().item().children(userId)).items.map(item => item.id);
		};

		if (deleteFromOwner) {
			await models().item().deleteForUser(owner.id, folderItem);

			// Deleting from the owner should delete the folder for both clients
			expect(await itemIdsForUser(owner.id)).toHaveLength(0);
			expect(await itemIdsForUser(recipient.id)).toHaveLength(0);

			// Share should be removed
			expect(await models().share().all()).toHaveLength(0);
		} else {
			// Delete from the recipient account
			await models().item().deleteForUser(recipient.id, folderItem);

			// Deleting from the recipient should delete the folder for just the recipient
			expect(await itemIdsForUser(owner.id)).toContain(folderItem.id);
			expect(await itemIdsForUser(recipient.id)).toHaveLength(0);

			// The share should still exist since the owner hasn't cancelled the
			// share.
			expect(await models().share().all()).toHaveLength(1);
		}

		// The notebook should no longer be shared with anyone.
		expect(await models().shareUser().all()).toHaveLength(0);
	});

	test('should unshare when the share object is deleted', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		expect((await models().item().children(user2.id)).items.length).toBe(2);

		const share = (await models().share().all())[0];
		await models().share().delete(share.id);

		expect((await models().item().children(user1.id)).items.length).toBe(2);
		expect((await models().item().children(user2.id)).items.length).toBe(0);
	});

	test('should see delta changes for linked items', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		let cursor1: string = null;
		let cursor2: string = null;

		{
			const names = ['000000000000000000000000000000F1.md', '00000000000000000000000000000001.md'].sort();

			const page1 = await getApi<PaginatedDeltaChanges>(session1.id, 'items/root/delta');
			expect(page1.items.map(i => i.item_name).sort()).toEqual(names);
			cursor1 = page1.cursor;

			const page2 = await getApi<PaginatedDeltaChanges>(session2.id, 'items/root/delta');
			expect(page2.items.map(i => i.item_name).sort()).toEqual(names);
			cursor2 = page2.cursor;
		}

		// --------------------------------------------------------------------
		// If item is changed on sharer side, sharee should see the changes
		// --------------------------------------------------------------------

		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
		const note: NoteEntity = await models().item().loadAsJoplinItem(noteItem.id);
		await msleep(1);
		await updateItem(session1.id, 'root:/00000000000000000000000000000001.md:', makeNoteSerializedBody({
			...note,
			title: 'modified by user 1',
		}));

		{
			const page1 = await getApi<PaginatedDeltaChanges>(session1.id, 'items/root/delta', { query: { cursor: cursor1 } });
			expect(page1.items.length).toBe(1);
			expect(page1.items[0].item_name).toBe('00000000000000000000000000000001.md');
			expect(page1.items[0].type).toBe(ChangeType.Update);
			cursor1 = page1.cursor;

			const page2 = await getApi<PaginatedDeltaChanges>(session2.id, 'items/root/delta', { query: { cursor: cursor2 } });
			expect(page2.items.length).toBe(1);
			expect(page2.items[0].item_name).toBe('00000000000000000000000000000001.md');
			expect(page2.items[0].type).toBe(ChangeType.Update);
			expect(page2.items[0].updated_time).toBe(page1.items[0].updated_time);
			cursor2 = page2.cursor;
		}

		// --------------------------------------------------------------------
		// If item is changed on sharee side, sharer should see the changes
		// --------------------------------------------------------------------

		await msleep(1);
		await updateItem(session2.id, 'root:/00000000000000000000000000000001.md:', makeNoteSerializedBody({
			...note,
			title: 'modified by user 2',
		}));

		{
			const page1 = await getApi<PaginatedDeltaChanges>(session1.id, 'items/root/delta', { query: { cursor: cursor1 } });
			expect(page1.items.length).toBe(1);
			expect(page1.items[0].item_name).toBe('00000000000000000000000000000001.md');
			expect(page1.items[0].type).toBe(ChangeType.Update);
			cursor1 = page1.cursor;

			const page2 = await getApi<PaginatedDeltaChanges>(session2.id, 'items/root/delta', { query: { cursor: cursor2 } });
			expect(page2.items.length).toBe(1);
			expect(page2.items[0].item_name).toBe('00000000000000000000000000000001.md');
			expect(page2.items[0].type).toBe(ChangeType.Update);
			cursor2 = page2.cursor;
		}
	});

	test('should get delta changes - user 2 sync, user 1 share and sync, user 2 sync', async () => {
		// - User 1 sync
		// - User 2 sync - and keep delta2
		// - User 1 share a folder with user 2
		// - User 2 accepts
		// - User 2 sync from delta2
		// => Should get shared folder and its content

		// When fetching changes - should add all user_items that have been created since delta2
		// And emit Create event for associated item ID

		const { session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		await createItemTree(user2.id, '', {
			'200000000000000000000000000000F2': {},
		});

		let latestChange2 = await models().change().delta(user2.id);
		const cursor2 = latestChange2.cursor;

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		latestChange2 = await models().change().delta(user2.id, { cursor: cursor2 });
		expect(latestChange2.items.length).toBe(2);
	});

	test('should get delta changes - user 1 and 2 are in sync, user 2 adds a note to shared folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		let latestChanges1 = await models().change().delta(user1.id);
		const cursor1 = latestChanges1.cursor;

		const folderItem1 = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');

		await createNote(session2.id, {
			id: '00000000000000000000000000000002',
			title: 'from user 2',
			parent_id: folderItem1.jop_id,
			share_id: share.id,
		});

		await models().share().updateSharedItems3();

		latestChanges1 = await models().change().delta(user1.id, { cursor: cursor1 });
		expect(latestChanges1.items.length).toBe(1);
		expect(latestChanges1.items[0].item_name).toBe('00000000000000000000000000000002.md');
	});

	test('should get delta changes - user 1 and 2 are in sync, user 2 moves a note out of shared folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		let latestChanges1 = await models().change().delta(user1.id);
		const cursor1 = latestChanges1.cursor;

		const folderItem2 = await createFolder(session2.id, { id: '000000000000000000000000000000F2', title: 'folder2' });
		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
		const note: NoteEntity = await models().item().loadAsJoplinItem(noteItem.id);

		await updateNote(session2.id, { ...note, parent_id: folderItem2.jop_id, share_id: '' });
		await models().share().updateSharedItems3();

		latestChanges1 = await models().change().delta(user1.id, { cursor: cursor1 });
		expect(latestChanges1.items.length).toBe(1);
		expect(latestChanges1.items[0].type).toBe(ChangeType.Delete);
		expect(latestChanges1.items[0].item_id).toBe(noteItem.id);
	});

	test('should get delta changes - user 1 and 2 are in sync, user 1 deletes invitation', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const resourceItem1 = await createResource(session1.id, { id: '000000000000000000000000000000E1' }, 'testing1');

		const { shareUser, share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
						title: 'note test',
						body: `[testing](:/${resourceItem1.jop_id})`,
					},
				],
			},
		]);

		await models().item().saveForUser(user1.id, {
			id: resourceItem1.id,
			jop_share_id: share.id,
		});

		const resourceBlob = await models().item().loadByName(user1.id, resourceBlobPath(resourceItem1.jop_id));

		await models().item().saveForUser(user1.id, {
			id: resourceBlob.id,
			jop_share_id: share.id,
		});

		await models().share().updateSharedItems3();

		expect((await models().userItem().byUserId(user2.id)).length).toBe(4);
		await deleteApi(session1.id, `share_users/${shareUser.id}`);


		expect((await models().userItem().byUserId(user2.id)).length).toBe(0);
	});

	test('should share an empty folder', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [],
			},
		]);
	});

	test('should unshare from a non-owner user who has deleted the root folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const { item } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		]);

		await models().share().updateSharedItems3();

		expect((await models().userItem().byUserId(user2.id)).length).toBe(2);

		await deleteApi(session2.id, `items/root:/${item.name}:`);

		expect((await models().userItem().byUserId(user1.id)).length).toBe(2);
		expect((await models().userItem().byUserId(user2.id)).length).toBe(0);
	});

	test('should not throw an error when deleting a root folder associated with a non-existing share', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);

		const { share, item } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [],
			},
		]);

		await db().from('shares').where('id', '=', share.id).delete();

		await expectNotThrow(async () => deleteApi(session2.id, `items/root:/${item.jop_id}.md:`));
	});

	test('should unshare a folder', async () => {
		// The process to unshare a folder is as follow:
		//
		// - Client call DELETE /api/share/:id
		// - Client sets the share_id of the folder and all sub-items to ""
		//
		// After doing this, when running updateSharedItems() on the server, it
		// will process a share that no longer exists. This is expected and
		// should not crash the process.

		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const { item, share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
			{
				id: '000000000000000000000000000000F1',
				children: [
					{
						id: '00000000000000000000000000000001',
					},
				],
			},
		]);

		await models().share().updateSharedItems3();

		expect((await models().userItem().byUserId(user2.id)).length).toBe(2);

		const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');

		// Simulate unsharing by setting the share ID to "" and deleting the share object
		await deleteApi(session1.id, `shares/${share.id}`);
		await models().item().saveForUser(user1.id, { id: item.id, jop_share_id: '' });
		await models().item().saveForUser(user1.id, { id: noteItem.id, jop_share_id: '' });

		await models().share().updateSharedItems3();

		expect((await models().userItem().byUserId(user2.id)).length).toBe(0);
	});

	test('should check permissions - cannot share a folder with yourself', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);

		await createItemTree(user1.id, '', { '000000000000000000000000000000F1': {} });
		const share = await postApi<Share>(session1.id, 'shares', { folder_id: '000000000000000000000000000000F1' });
		await expectHttpError(async () => postApi(session1.id, `shares/${share.id}/users`, { email: user1.email }), ErrorForbidden.httpCode);
	});

	test('should check permissions - cannot share a folder twice with a user', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { user: user2 } = await createUserAndSession(2);

		await createItemTree(user1.id, '', { '000000000000000000000000000000F1': {} });
		const share = await postApi<Share>(session1.id, 'shares', { folder_id: '000000000000000000000000000000F1' });
		await postApi(session1.id, `shares/${share.id}/users`, { email: user2.email });
		await expectHttpError(async () => postApi(session1.id, `shares/${share.id}/users`, { email: user2.email }), ErrorForbidden.httpCode);
	});

	test('should check permissions - cannot share a non-root folder', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);

		await createItemTree(user1.id, '', {
			'000000000000000000000000000000F1': {
				'000000000000000000000000000000F2': {},
			},
		});

		await expectHttpError(async () => postApi<Share>(session1.id, 'shares', { folder_id: '000000000000000000000000000000F2' }), ErrorForbidden.httpCode);
	});

	test('should check permissions - cannot share if share feature not enabled', async () => {
		const { user: user1, session: session1 } = await createUserAndSession(1);
		const { session: session2 } = await createUserAndSession(2);
		await models().user().save({ id: user1.id, can_share_folder: 0 });

		await expectHttpError(async () =>
			shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
				{
					id: '000000000000000000000000000000F1',
					children: [],
				},
			]),
		ErrorForbidden.httpCode,
		);
	});

	test('should check permissions - cannot share if share feature not enabled for recipient', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);
		await models().user().save({ id: user2.id, can_receive_folder: 0 });

		await expectHttpError(async () =>
			shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
				{
					id: '000000000000000000000000000000F1',
					children: [],
				},
			]),
		ErrorForbidden.httpCode,
		);
	});

	test('should check permissions - by default sharing by note is always possible', async () => {
		const { session: session1 } = await createUserAndSession(1);

		const noteItem = await createNote(session1.id, {
			title: 'Testing title',
			body: 'Testing body',
		});

		const share = await postApi<Share>(session1.id, 'shares', {
			type: ShareType.Note,
			note_id: noteItem.jop_id,
		});

		expect(share).toBeTruthy();
	});

	test('should check permissions - cannot share with a disabled account', async () => {
		const { session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);
		await models().user().save({
			id: user2.id,
			enabled: 0,
		});

		await expectHttpError(async () =>
			shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
				{
					id: '000000000000000000000000000000F1',
					children: [],
				},
			]),
		ErrorForbidden.httpCode,
		);
	});

	test('should allow sharing, unsharing and sharing again', async () => {
		// - U1 share a folder that contains a note
		// - U2 accept
		// - U2 syncs
		// - U1 remove U2
		// - U1 adds back U2
		// - U2 accept
		//
		// => Previously, the notebook would be deleted fro U2 due to a quirk in
		// delta sync, that doesn't handle user_items being deleted, then
		// created again. Instead U2 should end up with both the folder and the
		// note.
		//
		// Ref: https://discourse.joplinapp.org/t/20977

		const { session: session1 } = await createUserAndSession(1);
		const { user: user2, session: session2 } = await createUserAndSession(2);

		const { shareUser: shareUserA, share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
			'000000000000000000000000000000F1': {
				'00000000000000000000000000000001': null,
			},
		});

		await models().share().updateSharedItems3();

		await deleteApi(session1.id, `share_users/${shareUserA.id}`);

		await models().share().updateSharedItems3();

		await inviteUserToShare(share, session1.id, user2.email, true);

		await models().share().updateSharedItems3();

		const page = await getApi<PaginatedDeltaChanges>(session2.id, 'items/root/delta', { query: { cursor: '' } });

		expect(page.items.length).toBe(2);
		expect(page.items.find(it => it.item_name === '000000000000000000000000000000F1.md').type).toBe(ChangeType.Create);
		expect(page.items.find(it => it.item_name === '00000000000000000000000000000001.md').type).toBe(ChangeType.Create);
	});

});
