import { setupDatabaseAndSynchronizer, switchClient, createFolderTree, supportDir, msleep, resourceService, simulateReadOnlyShareEnv } from '../testing/test-utils';
import Folder from '../models/Folder';
import { allNotesFolders } from '../testing/test-utils-synchronizer';
import Note from '../models/Note';
import shim from '../shim';
import Resource from '../models/Resource';
import { FolderEntity, NoteEntity, ResourceEntity } from '../services/database/types';
import ResourceService from '../services/ResourceService';
import { StateShare } from '../services/share/reducer';

const testImagePath = `${supportDir}/photo.jpg`;

describe('models/Folder.sharing', () => {

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

	it('should apply the share ID to all children', (async () => {
		const folder = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
					{
						title: 'folder 2',
						children: [
							{
								title: 'note 3',
							},
						],
					},
				],
			},
		]);

		await Folder.save({ id: folder.id, share_id: 'abcd1234' });
		await Folder.updateAllShareIds(resourceService(), []);

		const allItems = await allNotesFolders();
		for (const item of allItems) {
			expect(item.share_id).toBe('abcd1234');
		}
	}));

	it('should apply the share ID to all sub-folders', (async () => {
		let folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
					{
						title: 'folder 2',
						children: [
							{
								title: 'note 3',
							},
						],
					},
					{
						title: 'folder 3',
						children: [
							{
								title: 'folder 4',
								children: [],
							},
						],
					},
				],
			},
			{
				title: 'folder 5',
				children: [],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });

		await Folder.updateAllShareIds(resourceService(), []);

		folder1 = await Folder.loadByTitle('folder 1');
		const folder2 = await Folder.loadByTitle('folder 2');
		const folder3 = await Folder.loadByTitle('folder 3');
		const folder4 = await Folder.loadByTitle('folder 4');
		const folder5 = await Folder.loadByTitle('folder 5');

		expect(folder1.share_id).toBe('abcd1234');
		expect(folder2.share_id).toBe('abcd1234');
		expect(folder3.share_id).toBe('abcd1234');
		expect(folder4.share_id).toBe('abcd1234');
		expect(folder5.share_id).toBe('');
	}));

	it('should clear the share ID of a folder immediately when moved out of a shared folder', async () => {
		let folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'folder 2',
						children: [
							{
								title: 'folder 3',
								children: [],
							},
						],
					},
				],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'test123456' });
		await Folder.updateAllShareIds(resourceService(), []);

		folder1 = await Folder.loadByTitle('folder 1');
		const folder2 = await Folder.loadByTitle('folder 2');

		expect(folder1.share_id).toBe('test123456');
		expect(folder2.share_id).toBe('test123456');

		await Folder.moveToFolder(folder2.id, '');
		// Should have updated the share_id of folder 2 during "moveToFolder":
		expect(await Folder.loadByTitle('folder 2')).toMatchObject({
			share_id: '',
		});
	});

	it('should update the share ID when a folder is moved in or out of shared folder', (async () => {
		let folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'folder 2',
						children: [],
					},
				],
			},
			{
				title: 'folder 3',
				children: [],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });

		await Folder.updateAllShareIds(resourceService(), []);

		folder1 = await Folder.loadByTitle('folder 1');
		let folder2 = await Folder.loadByTitle('folder 2');
		const folder3 = await Folder.loadByTitle('folder 3');

		expect(folder1.share_id).toBe('abcd1234');
		expect(folder2.share_id).toBe('abcd1234');

		// Move the folder outside the shared folder

		await Folder.save({ id: folder2.id, parent_id: folder3.id });
		await Folder.updateAllShareIds(resourceService(), []);
		folder2 = await Folder.loadByTitle('folder 2');
		expect(folder2.share_id).toBe('');

		// Move the folder inside the shared folder

		{
			await Folder.save({ id: folder2.id, parent_id: folder1.id });
			await Folder.updateAllShareIds(resourceService(), []);
			folder2 = await Folder.loadByTitle('folder 2');
			expect(folder2.share_id).toBe('abcd1234');
		}
	}));

	it('should successfully clear the share ID of a no-longer-shared item in a readonly share', async () => {
		const root = await Folder.save({
			title: 'unshared root',
			share_id: '',
		});

		const shareId = 'some-share-id-here';
		const folder = await Folder.save({
			title: 'Unshared, but still with an old share ID',
			share_id: shareId,
			parent_id: root.id,
		});

		const reset = simulateReadOnlyShareEnv([shareId]);
		try {
			await Folder.updateAllShareIds(
				resourceService(),
				Folder.syncShareCache.shares,
			);
		} finally {
			reset();
		}

		expect(await Folder.load(folder.id)).toMatchObject({
			share_id: '',
		});
	});

	it('updating share IDs should not fail when a read-only resource is linked to a writeable note', async () => {
		const root = await Folder.save({ title: 'read-only' });
		let note = await Note.save({ title: 'Test', parent_id: root.id });
		note = await shim.attachFileToNote(note, testImagePath);
		const resourceId = Note.linkedItemIds(note.body)[0];

		// Associate the resource with the share
		const shareId = 'abcd1234';
		await Resource.save({
			id: resourceId,
			share_id: shareId,
			is_shared: 1,
		});

		const reset = simulateReadOnlyShareEnv([shareId]);
		try {
			await Folder.updateAllShareIds(resourceService(), []);
		} finally {
			reset();
		}
	});

	it('should not fail to update note share IDs when an outdated share ID is contained in a read-only folder', async () => {
		const shareId = 'abcd1234';
		const root = await Folder.save({ title: 'read-only', share_id: shareId });
		// Save a child with a different share ID
		const child = await Note.save({ title: 'Test', parent_id: root.id, share_id: `${shareId}-different` });

		const reset = simulateReadOnlyShareEnv([shareId]);
		try {
			await Folder.updateAllShareIds(resourceService(), []);
			expect(await Note.load(child.id)).toMatchObject({ share_id: shareId });
		} finally {
			reset();
		}
	});

	it('should unshare a subfolder of a shared folder when it is moved to the root', (async () => {
		let folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'folder 2',
						children: [],
					},
				],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });

		const stateShares: StateShare[] = [
			{
				folder_id: folder1.id,
				id: 'abcd1234',
				master_key_id: '',
				note_id: '',
				type: 3,
			},
		];

		await Folder.updateAllShareIds(resourceService(), stateShares);

		folder1 = await Folder.loadByTitle('folder 1');
		let folder2 = await Folder.loadByTitle('folder 2');

		expect(folder1.share_id).toBe('abcd1234');
		expect(folder2.share_id).toBe('abcd1234');

		// Move the subfolder to the root

		await Folder.save({ id: folder2.id, parent_id: '' });
		await Folder.updateAllShareIds(resourceService(), stateShares);
		folder2 = await Folder.loadByTitle('folder 2');
		expect(folder2.share_id).toBe('');
	}));

	it('should apply the share ID to all notes', (async () => {
		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
					{
						title: 'folder 2',
						children: [
							{
								title: 'note 3',
							},
						],
					},
				],
			},
			{
				title: 'folder 5',
				children: [
					{
						title: 'note 4',
					},
				],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });

		await Folder.updateAllShareIds(resourceService(), []);

		const note1: NoteEntity = await Note.loadByTitle('note 1');
		const note2: NoteEntity = await Note.loadByTitle('note 2');
		const note3: NoteEntity = await Note.loadByTitle('note 3');
		const note4: NoteEntity = await Note.loadByTitle('note 4');

		expect(note1.share_id).toBe('abcd1234');
		expect(note2.share_id).toBe('abcd1234');
		expect(note3.share_id).toBe('abcd1234');
		expect(note4.share_id).toBe('');
	}));

	it('should remove the share ID when a note is moved in or out of shared folder', (async () => {
		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
				],
			},
			{
				title: 'folder 2',
				children: [],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
		await Folder.updateAllShareIds(resourceService(), []);
		const note1: NoteEntity = await Note.loadByTitle('note 1');
		const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
		expect(note1.share_id).toBe('abcd1234');

		// Move the note outside of the shared folder

		await Note.save({ id: note1.id, parent_id: folder2.id });
		await Folder.updateAllShareIds(resourceService(), []);

		{
			const note1: NoteEntity = await Note.loadByTitle('note 1');
			expect(note1.share_id).toBe('');
		}

		// Move the note back inside the shared folder

		await Note.save({ id: note1.id, parent_id: folder1.id });
		await Folder.updateAllShareIds(resourceService(), []);

		{
			const note1: NoteEntity = await Note.loadByTitle('note 1');
			expect(note1.share_id).toBe('abcd1234');
		}
	}));

	it('should not remove the share ID of non-modified notes', (async () => {
		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
				],
			},
			{
				title: 'folder 2',
				children: [],
			},
		]);

		await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
		await Folder.updateAllShareIds(resourceService(), []);

		let note1: NoteEntity = await Note.loadByTitle('note 1');
		let note2: NoteEntity = await Note.loadByTitle('note 2');
		const folder2: FolderEntity = await Folder.loadByTitle('folder 2');

		expect(note1.share_id).toBe('abcd1234');
		expect(note2.share_id).toBe('abcd1234');

		await Note.save({ id: note1.id, parent_id: folder2.id });
		await Folder.updateAllShareIds(resourceService(), []);

		note1 = await Note.loadByTitle('note 1');
		note2 = await Note.loadByTitle('note 2');
		expect(note1.share_id).toBe('');
		expect(note2.share_id).toBe('abcd1234');
	}));

	it('should apply the note share ID to its resources', async () => {
		const resourceService = new ResourceService();

		const folder = await createFolderTree('', [
			{
				title: 'folder 1',
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
				],
			},
			{
				title: 'folder 2',
				children: [],
			},
		]);

		await Folder.save({ id: folder.id, share_id: 'abcd1234' });
		await Folder.updateAllShareIds(resourceService, []);

		const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
		const note1: NoteEntity = await Note.loadByTitle('note 1');
		await shim.attachFileToNote(note1, testImagePath);

		// We need to index the resources to populate the note_resources table
		await resourceService.indexNoteResources();

		const resourceId: string = (await Resource.all())[0].id;

		{
			const resource: ResourceEntity = await Resource.load(resourceId);
			expect(resource.share_id).toBe('');
		}

		const previousBlobUpdatedTime = (await Resource.load(resourceId)).blob_updated_time;
		await msleep(1);
		await Folder.updateAllShareIds(resourceService, []);

		{
			const resource: ResourceEntity = await Resource.load(resourceId);
			expect(resource.share_id).toBe(note1.share_id);
			expect(resource.blob_updated_time).toBeGreaterThan(previousBlobUpdatedTime);
		}

		await Note.save({ id: note1.id, parent_id: folder2.id });
		await resourceService.indexNoteResources();

		await Folder.updateAllShareIds(resourceService, []);

		{
			const resource: ResourceEntity = await Resource.load(resourceId);
			expect(resource.share_id).toBe('');
		}
	});

	it('should automatically duplicate resources when they are shared', async () => {
		const resourceService = new ResourceService();

		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1', // SHARE 1
				children: [
					{
						title: 'note 1',
					},
					{
						title: 'note 2',
					},
				],
			},
			{
				title: 'folder 2', // SHARE 2
				children: [
					{
						title: 'note 3',
					},
				],
			},
			{
				title: 'folder 3', // (not shared)
				children: [
					{
						title: 'note 4',
					},
				],
			},
		]);

		const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
		// await Folder.loadByTitle('folder 3');
		let note1: NoteEntity = await Note.loadByTitle('note 1');
		let note2: NoteEntity = await Note.loadByTitle('note 2');
		let note3: NoteEntity = await Note.loadByTitle('note 3');
		let note4: NoteEntity = await Note.loadByTitle('note 4');

		await Folder.save({ id: folder1.id, share_id: 'share1' });
		await Folder.save({ id: folder2.id, share_id: 'share2' });

		note1 = await shim.attachFileToNote(note1, testImagePath);
		note2 = await shim.attachFileToNote(note2, testImagePath);
		note3 = await Note.save({ id: note3.id, body: note1.body });
		note4 = await Note.save({ id: note4.id, body: note1.body });

		const userUpdatedTimes: Record<string, number> = {
			[note1.id]: note1.user_updated_time,
			[note2.id]: note2.user_updated_time,
			[note3.id]: note3.user_updated_time,
			[note4.id]: note4.user_updated_time,
		};

		await msleep(1);

		// We need to index the resources to populate the note_resources table

		await resourceService.indexNoteResources();
		await Folder.updateAllShareIds(resourceService, []);

		// BEFORE:
		//
		// - Note 1 has resource 1 (share1)
		// - Note 2 has resource 2 (share1)
		// - Note 3 has resource 1 (share2)
		// - Note 4 has resource 1 (not shared)

		// AFTER:
		//
		// - Note 1 has resource 1 (share1)
		// - Note 2 has resource 2 (share1)
		// - Note 3 has resource 3 (share2)
		// - Note 4 has resource 4 (not shared)

		const resources = await Resource.all();
		expect(resources.length).toBe(4);

		note1 = await Note.load(note1.id);
		note2 = await Note.load(note2.id);
		note3 = await Note.load(note3.id);
		note4 = await Note.load(note4.id);

		expect(note1.body).not.toBe(note2.body);
		expect(note1.body).not.toBe(note3.body);
		expect(note1.body).not.toBe(note4.body);
		expect(note2.body).not.toBe(note3.body);
		expect(note2.body).not.toBe(note4.body);
		expect(note3.body).not.toBe(note4.body);

		expect(note1.user_updated_time).toBe(userUpdatedTimes[note1.id]);
		expect(note2.user_updated_time).toBe(userUpdatedTimes[note2.id]);
		expect(note3.user_updated_time).toBe(userUpdatedTimes[note3.id]);
		expect(note4.user_updated_time).toBe(userUpdatedTimes[note4.id]);
	});

	it('should prefer duplicating resources in unshared folders to shared folders', async () => {
		const resourceService = new ResourceService();

		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1', // Share 1
				children: [
					{
						title: 'note 1',
					},
				],
			},
			{
				title: 'folder 2', // Not shared
				children: [
					{
						title: 'note 2',
					},
				],
			},
		]);

		let note1: NoteEntity = await Note.loadByTitle('note 1');
		let note2: NoteEntity = await Note.loadByTitle('note 2');

		await Folder.save({ id: folder1.id, share_id: 'share1' });

		note1 = await shim.attachFileToNote(note1, testImagePath);
		note2 = await Note.save({ id: note2.id, body: note1.body });

		await msleep(1);

		await resourceService.indexNoteResources(); // Populate note_resources
		await Folder.updateAllShareIds(resourceService, []);

		// After
		expect(await Resource.all()).toHaveLength(2);

		// note1 should have the same body
		expect(await Note.load(note1.id)).toMatchObject({ body: note1.body, share_id: 'share1' });
		// note2's body should be updated
		expect(await Note.load(note2.id)).not.toMatchObject({ body: note2.body, share_id: '' });
	});


	it('should not duplicate a resource if both instances are in different read-only shares', async () => {
		const resourceService = new ResourceService();

		const folder1 = await createFolderTree('', [
			{
				title: 'folder 1', // Share 1
				children: [
					{
						title: 'note 1',
					},
				],
			},
			{
				title: 'folder 2', // Share 2
				children: [
					{
						title: 'note 2',
					},
				],
			},
		]);

		const folder2 = await Folder.loadByTitle('folder 2');

		let note1: NoteEntity = await Note.loadByTitle('note 1');
		const note2: NoteEntity = await Note.loadByTitle('note 2');
		note1 = await shim.attachFileToNote(note1, testImagePath);
		await Note.save({ id: note2.id, body: note1.body });

		const shareId1 = 'testing1';
		const shareId2 = 'testing2';
		await Folder.save({ id: folder1.id, share_id: shareId1 });
		await Folder.save({ id: folder2.id, share_id: shareId2 });

		await msleep(1);

		const reset = simulateReadOnlyShareEnv([shareId1, shareId2]);
		try {
			await resourceService.indexNoteResources(); // Populate note_resources
			await Folder.updateAllShareIds(resourceService, []);

			// After: Should not have duplicated the resource
			expect(await Resource.all()).toHaveLength(1);
		} finally {
			reset();
		}
	});

	it('should clear share_ids for items that are no longer part of an existing share', async () => {
		await createFolderTree('', [
			{
				title: 'folder 1',
				share_id: '1',
				children: [
					{
						title: 'note 1',
					},
				],
			},
			{
				title: 'folder 2',
				share_id: '2',
				children: [
					{
						title: 'note 2',
					},
				],
			},
		]);

		const resourceService = new ResourceService();

		const folder1: FolderEntity = await Folder.loadByTitle('folder 1');
		const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
		let note1: NoteEntity = await Note.loadByTitle('note 1');
		let note2: NoteEntity = await Note.loadByTitle('note 2');
		note1 = await shim.attachFileToNote(note1, testImagePath);
		note2 = await shim.attachFileToNote(note2, testImagePath);
		const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
		const resourceId2 = (await Note.linkedResourceIds(note2.body))[0];

		await resourceService.indexNoteResources();

		await Folder.updateAllShareIds(resourceService, []);

		await Folder.updateNoLongerSharedItems(['1']);

		// Since `updateNoLongerSharedItems` sets the parent_id too,
		// double-check that it's not actually modified.
		expect((await Note.loadByTitle('note 1')).parent_id).toBe(folder1.id);
		expect((await Note.loadByTitle('note 2')).parent_id).toBe(folder2.id);
		expect((await Folder.loadByTitle('folder 1')).parent_id).toBe(folder1.parent_id);
		expect((await Folder.loadByTitle('folder 2')).parent_id).toBe(folder2.parent_id);

		// At this point, all items associated with share 2 should have their
		// share_id cleared, because the share no longer exists. We also
		// double-check that share 1 hasn't been cleared.
		expect((await Note.loadByTitle('note 1')).share_id).toBe('1');
		expect((await Note.loadByTitle('note 2')).share_id).toBe('');
		expect((await Folder.loadByTitle('folder 1')).share_id).toBe('1');
		expect((await Folder.loadByTitle('folder 2')).share_id).toBe('');
		expect((await Resource.load(resourceId1)).share_id).toBe('1');
		expect((await Resource.load(resourceId2)).share_id).toBe('');

		// If we pass an empty array, it means there are no active share
		// anymore, so all share_id should be cleared.
		await Folder.updateNoLongerSharedItems([]);
		expect((await Note.loadByTitle('note 1')).share_id).toBe('');
		expect((await Folder.loadByTitle('folder 1')).share_id).toBe('');
		expect((await Resource.load(resourceId1)).share_id).toBe('');

		{
			// If we run it again, it should not update the notes since the share_id
			// has already been cleared.
			const resource1 = await Resource.load(resourceId1);
			const resource2 = await Resource.load(resourceId2);
			const note1 = await Note.loadByTitle('note 1');
			const note2 = await Note.loadByTitle('note 2');
			const folder1 = await Folder.loadByTitle('folder 1');
			const folder2 = await Folder.loadByTitle('folder 2');

			await msleep(1);

			await Folder.updateNoLongerSharedItems(['1']);

			expect((await Resource.load(resourceId1)).updated_time).toBe(resource1.updated_time);
			expect((await Resource.load(resourceId2)).updated_time).toBe(resource2.updated_time);
			expect((await Note.loadByTitle('note 1')).updated_time).toBe(note1.updated_time);
			expect((await Note.loadByTitle('note 2')).updated_time).toBe(note2.updated_time);
			expect((await Folder.loadByTitle('folder 1')).updated_time).toBe(folder1.updated_time);
			expect((await Folder.loadByTitle('folder 2')).updated_time).toBe(folder2.updated_time);
		}
	});

	it('should not change the updated_time when clearing share_ids for no-longer-shared items', async () => {
		await createFolderTree('', [
			{
				title: 'folder 1',
				share_id: '1',
				children: [
					{
						title: 'note 1',
					},
				],
			},
		]);

		await Folder.updateAllShareIds(resourceService(), []);

		const folder1 = await Folder.loadByTitle('folder 1');
		const note1 = await Note.loadByTitle('note 1');
		await Folder.updateNoLongerSharedItems([]);

		// To avoid conflicts, should not change the updated_time
		expect(await Note.loadByTitle('note 1')).toMatchObject({
			updated_time: note1.updated_time,
			share_id: '',
		});
		expect(await Folder.loadByTitle('folder 1')).toMatchObject({
			updated_time: folder1.updated_time,
			share_id: '',
		});
	});

});
