import Setting from '../../models/Setting';
import BaseModel from '../../BaseModel';
import { synchronizerStart, revisionService, setupDatabaseAndSynchronizer, synchronizer, switchClient, encryptionService, loadEncryptionMasterKey, decryptionWorker } from '../../testing/test-utils';
import Note from '../../models/Note';
import Revision from '../../models/Revision';
import { loadMasterKeysFromSettings, setupAndEnableEncryption } from '../e2ee/utils';
import { onRevisionServiceSettingsChanged } from './syncInfoUtils';

describe('Synchronizer.revisions', () => {

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

	it('should not save revisions when updating a note via sync', (async () => {
		// When a note is updated, a revision of the original is created.
		// Here, on client 1, the note is updated for the first time, however since it is
		// via sync, we don't create a revision - that revision has already been created on client
		// 2 and is going to be synced.

		const n1 = await Note.save({ title: 'testing' });
		await synchronizerStart();

		await switchClient(2);

		await synchronizerStart();
		await Note.save({ id: n1.id, title: 'mod from client 2' });
		await revisionService().collectRevisions();
		const allRevs1 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
		expect(allRevs1.length).toBe(1);
		await synchronizerStart();

		await switchClient(1);

		await synchronizerStart();
		const allRevs2 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
		expect(allRevs2.length).toBe(1);
		expect(allRevs2[0].id).toBe(allRevs1[0].id);
	}));

	it('should not save revisions when deleting a note via sync', (async () => {
		const n1 = await Note.save({ title: 'testing' });
		await synchronizerStart();

		await switchClient(2);

		await synchronizerStart();
		await Note.delete(n1.id);
		await revisionService().collectRevisions(); // One revision is created here because it is an "old note", see revisionService
		{
			const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
			expect(allRevs.length).toBe(1);
		}
		await synchronizerStart();

		await switchClient(1);

		await synchronizerStart(); // The local note gets deleted here, however a new rev is *not* created
		{
			const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
			expect(allRevs.length).toBe(0);
		}

		const notes = await Note.all();
		expect(notes.length).toBe(0);
	}));

	it('should not save revisions when an item_change has been generated as a result of a sync', (async () => {
		// When a note is modified an item_change object is going to be created. This
		// is used for example to tell the search engine, when note should be indexed. It is
		// also used by the revision service to tell what note should get a new revision.
		// When a note is modified via sync, this item_change object is also created. The issue
		// is that we don't want to create revisions for these particular item_changes, because
		// such revision has already been created on another client (whatever client initially
		// modified the note), and that rev is going to be synced.
		//
		// So in the end we need to make sure that we don't create these unnecessary additional revisions.

		const n1 = await Note.save({ title: 'testing' });
		await synchronizerStart();

		await switchClient(2);

		await synchronizerStart();
		await Note.save({ id: n1.id, title: 'mod from client 2' });
		await revisionService().collectRevisions();
		await synchronizerStart();

		await switchClient(1);

		await synchronizerStart();

		{
			const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
			expect(allRevs.length).toBe(1);
		}

		await revisionService().collectRevisions();

		{
			const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
			expect(allRevs.length).toBe(1);
		}
	}));

	it('should handle case when new rev is created on client, then older rev arrives later via sync', (async () => {
		// - C1 creates note 1
		// - C1 modifies note 1 - REV1 created
		// - C1 sync
		// - C2 sync
		// - C2 receives note 1
		// - C2 modifies note 1 - REV2 created (but not based on REV1)
		// - C2 receives REV1
		//
		// In that case, we need to make sure that REV1 and REV2 are both valid and can be retrieved.
		// Even though REV1 was created before REV2, REV2 is *not* based on REV1. This is not ideal
		// due to unnecessary data being saved, but a possible edge case and we simply need to check
		// all the data is valid.

		// Note: this test seems to be a bit shaky because it doesn't work if the synchronizer
		// context is passed around (via synchronizerStart()), but it should.

		const n1 = await Note.save({ title: 'note' });
		await Note.save({ id: n1.id, title: 'note REV1' });
		await revisionService().collectRevisions(); // REV1
		expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
		await synchronizer().start();

		await switchClient(2);

		synchronizer().testingHooks_ = ['skipRevisions'];
		await synchronizer().start();
		synchronizer().testingHooks_ = [];

		await Note.save({ id: n1.id, title: 'note REV2' });
		await revisionService().collectRevisions(); // REV2
		expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
		await synchronizer().start(); // Sync the rev that had been skipped above with skipRevisions

		const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
		expect(revisions.length).toBe(2);

		expect((await revisionService().revisionNote(revisions, 0)).title).toBe('note REV1');
		expect((await revisionService().revisionNote(revisions, 1)).title).toBe('note REV2');
	}));

	it('should not create revisions when item is modified as a result of decryption', (async () => {
		// Handle this scenario:
		// - C1 creates note
		// - C1 never changes it
		// - E2EE is enabled
		// - C1 sync
		// - More than one week later (as defined by oldNoteCutOffDate_), C2 sync
		// - C2 enters master password and note gets decrypted
		//
		// Technically at this point the note is modified (from encrypted to non-encrypted) and thus a ItemChange
		// object is created. The note is also older than oldNoteCutOffDate. However, this should not lead to the
		// creation of a revision because that change was not the result of a user action.
		// I guess that's the general rule - changes that come from user actions should result in revisions,
		// while automated changes (sync, decryption) should not.

		const dateInPast = revisionService().oldNoteCutOffDate_() - 1000;

		await Note.save({ title: 'ma note', updated_time: dateInPast, created_time: dateInPast }, { autoTimestamp: false });
		const masterKey = await loadEncryptionMasterKey();
		await setupAndEnableEncryption(encryptionService(), masterKey, '123456');
		await loadMasterKeysFromSettings(encryptionService());
		await synchronizerStart();

		await switchClient(2);

		await synchronizerStart();

		Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
		await loadMasterKeysFromSettings(encryptionService());
		await decryptionWorker().start();

		await revisionService().collectRevisions();

		expect((await Revision.all()).length).toBe(0);
	}));

	it('should delete old revisions remotely when deleted locally', async () => {
		Setting.setValue('revisionService.intervalBetweenRevisions', 100);
		jest.useFakeTimers({ advanceTimers: true });

		const note = await Note.save({ title: 'note' });
		const getNoteRevisions = () => {
			return Revision.allByType(BaseModel.TYPE_NOTE, note.id);
		};
		jest.advanceTimersByTime(200);

		await Note.save({ id: note.id, title: 'note REV0' });
		jest.advanceTimersByTime(200);

		await revisionService().collectRevisions(); // REV0
		expect(await getNoteRevisions()).toHaveLength(1);

		jest.advanceTimersByTime(200);

		await Note.save({ id: note.id, title: 'note REV1' });
		await revisionService().collectRevisions(); // REV1
		expect(await getNoteRevisions()).toHaveLength(2);

		// Should sync the revisions
		await synchronizer().start();
		await switchClient(2);
		await synchronizer().start();

		expect(await getNoteRevisions()).toHaveLength(2);
		await revisionService().deleteOldRevisions(100);
		expect(await getNoteRevisions()).toHaveLength(0);

		await synchronizer().start();
		expect(await getNoteRevisions()).toHaveLength(0);

		// Syncing a new client should not download the deleted revisions
		await setupDatabaseAndSynchronizer(3);
		await switchClient(3);
		await synchronizer().start();
		expect(await getNoteRevisions()).toHaveLength(0);

		// After switching back to the original client, syncing should locally delete
		// the remotely deleted revisions.
		await switchClient(1);
		expect(await getNoteRevisions()).toHaveLength(2);
		await synchronizer().start();
		expect(await getNoteRevisions()).toHaveLength(0);

		jest.useRealTimers();
	});

	it('should sync both deleted and merged revisions to remote, when revision deletion retains some revisions locally', async () => {
		// - C1 creates note 1
		// - C1 modifies note 1 over a period of time - 2 revisions are created
		// - C1 sync
		// - C2 sync
		// - C2 receives note 1 with the revisions
		// - C2 deletes the oldest of the 2 revisions, leaving 1 merged revision
		// - C2 sync
		// - C1 sync
		// - C1 receives 1 merged revision and the older one is deleted
		//
		// When at least one, but not all revisions are deleted for a note, the new oldest revision must be a merge of all
		// previous revisions which were deleted. So in addition to verifying that old revision deletions are synced so that
		// other clients will delete those revisions, we also need to verify that a merged revision is synced and is then updated
		// when another client receives it
		Setting.setValue('revisionService.intervalBetweenRevisions', 100);
		jest.useFakeTimers({ advanceTimers: true });

		const note = await Note.save({ title: 'note' });
		const getNoteRevisions = () => {
			return Revision.allByType(BaseModel.TYPE_NOTE, note.id);
		};
		jest.advanceTimersByTime(500);

		await Note.save({ id: note.id, title: 'note REV0' });
		jest.advanceTimersByTime(500);

		await revisionService().collectRevisions(); // REV0
		expect(await getNoteRevisions()).toHaveLength(1);

		const interimTime = Date.now();
		jest.advanceTimersByTime(500);

		await Note.save({ id: note.id, title: 'note REV1' });
		await revisionService().collectRevisions(); // REV1
		expect(await getNoteRevisions()).toHaveLength(2);

		// Should sync the revisions
		await synchronizerStart();
		await switchClient(2);
		await synchronizerStart();

		// Prevent a race condition whereby a revision is downloaded via the sync, then one of the same revisions is updated within the same millisecond via
		// deleteOldRevisions, and therefore is not uploaded via the sync because the sync_time matches
		jest.advanceTimersByTime(500);

		const revisions = await getNoteRevisions();
		expect(revisions).toHaveLength(2);
		expect(revisions[0].title_diff).toBe('[{"diffs":[[1,"note REV0"]],"start1":0,"start2":0,"length1":0,"length2":9}]');
		expect(revisions[1].title_diff).toBe('[{"diffs":[[0," REV"],[-1,"0"],[1,"1"]],"start1":4,"start2":4,"length1":5,"length2":5}]');

		await revisionService().deleteOldRevisions(Date.now() - interimTime);
		expect(await getNoteRevisions()).toHaveLength(1);

		await synchronizerStart();
		expect(await getNoteRevisions()).toHaveLength(1);

		// After switching back to the original client, syncing should locally delete
		// the remotely deleted revisions and update the merged revision.
		await switchClient(1);
		expect(await getNoteRevisions()).toHaveLength(2);
		await synchronizerStart();

		const revisionsAfterSync = await getNoteRevisions();
		expect(revisionsAfterSync).toHaveLength(1);
		expect(revisionsAfterSync[0].title_diff).toBe('[{"diffs":[[1,"note REV1"]],"start1":0,"start2":0,"length1":0,"length2":9}]');
		expect(revisionsAfterSync[0].updated_time).toBeGreaterThan(revisions[0].updated_time);

		jest.useRealTimers();
	});

	it('should sync revision service settings across clients', (async () => {
		const changeSetting = (key: string, value: unknown) => {
			Setting.setValue(key, value);
			onRevisionServiceSettingsChanged(key, value);
		};

		changeSetting('revisionService.enabled', false);
		await synchronizerStart();
		await switchClient(2);

		expect(Setting.value('revisionService.enabled')).toBe(true);
		await synchronizerStart();
		expect(Setting.value('revisionService.enabled')).toBe(false);

		changeSetting('revisionService.enabled', true);
		await synchronizerStart();
		await switchClient(1);

		expect(Setting.value('revisionService.enabled')).toBe(false);
		await synchronizerStart();
		expect(Setting.value('revisionService.enabled')).toBe(true);
	}));
});
