"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const time_1 = require("../../time");
const test_utils_synchronizer_1 = require("../../testing/test-utils-synchronizer");
const test_utils_1 = require("../../testing/test-utils");
const Folder_1 = require("../../models/Folder");
const Note_1 = require("../../models/Note");
const BaseItem_1 = require("../../models/BaseItem");
const syncInfoUtils_1 = require("../synchronizer/syncInfoUtils");
describe('Synchronizer.conflicts', () => {
    beforeEach(async () => {
        await (0, test_utils_1.setupDatabaseAndSynchronizer)(1);
        await (0, test_utils_1.setupDatabaseAndSynchronizer)(2);
        await (0, test_utils_1.switchClient)(1);
    });
    it('should resolve note conflicts', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        const note1 = await Note_1.default.save({ title: 'un', parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        let syncItem = await BaseItem_1.default.syncItem((0, test_utils_1.syncTargetId)(), note1.id, { fields: ['sync_time'] });
        expect(syncItem.sync_time).toBe(note1.updated_time);
        let note2 = await Note_1.default.load(note1.id);
        note2.title = 'Updated on client 2';
        await Note_1.default.save(note2);
        note2 = await Note_1.default.load(note2.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        let note2conf = await Note_1.default.load(note1.id);
        note2conf.title = 'Updated on client 1';
        await Note_1.default.save(note2conf);
        note2conf = await Note_1.default.load(note1.id);
        await (0, test_utils_1.synchronizerStart)();
        syncItem = await BaseItem_1.default.syncItem((0, test_utils_1.syncTargetId)(), note2.id, { fields: ['sync_time'] });
        expect(syncItem.sync_time).toBe(note2.updated_time);
        const conflictedNotes = await Note_1.default.conflictedNotes();
        expect(conflictedNotes.length).toBe(1);
        // Other than the id (since the conflicted note is a duplicate), and the is_conflict property
        // the conflicted and original note must be the same in every way, to make sure no data has been lost.
        const conflictedNote = conflictedNotes[0];
        expect(conflictedNote.id === note2conf.id).toBe(false);
        expect(conflictedNote.conflict_original_id).toBe(note2conf.id);
        for (const n in conflictedNote) {
            if (!conflictedNote.hasOwnProperty(n))
                continue;
            if (n === 'id' || n === 'is_conflict' || n === 'conflict_original_id')
                continue;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            expect(conflictedNote[n]).toBe(note2conf[n]);
        }
        const noteUpdatedFromRemote = await Note_1.default.load(note1.id);
        for (const n in noteUpdatedFromRemote) {
            if (!noteUpdatedFromRemote.hasOwnProperty(n))
                continue;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            expect(noteUpdatedFromRemote[n]).toBe(note2[n]);
        }
    }));
    it('should cap sync_time to the current device time in the delta step', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        const note1 = await Note_1.default.save({ title: 'un', parent_id: folder1.id, updated_time: time_1.default.unixMs() + 100000 }, { autoTimestamp: false });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        const syncItem = await BaseItem_1.default.syncItem((0, test_utils_1.syncTargetId)(), note1.id, { fields: ['sync_time'] });
        expect(syncItem.sync_time).toBeLessThan(note1.updated_time);
    }));
    it('should resolve folders conflicts', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        await Note_1.default.save({ title: 'un', parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2); // ----------------------------------
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.sleep)(0.1);
        let folder1_modRemote = await Folder_1.default.load(folder1.id);
        folder1_modRemote.title = 'folder1 UPDATE CLIENT 2';
        await Folder_1.default.save(folder1_modRemote);
        folder1_modRemote = await Folder_1.default.load(folder1_modRemote.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1); // ----------------------------------
        await (0, test_utils_1.sleep)(0.1);
        let folder1_modLocal = await Folder_1.default.load(folder1.id);
        folder1_modLocal.title = 'folder1 UPDATE CLIENT 1';
        await Folder_1.default.save(folder1_modLocal);
        folder1_modLocal = await Folder_1.default.load(folder1.id);
        await (0, test_utils_1.synchronizerStart)();
        const folder1_final = await Folder_1.default.load(folder1.id);
        expect(folder1_final.title).toBe(folder1_modRemote.title);
    }));
    it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        await Folder_1.default.delete(folder1.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        await Note_1.default.save({ title: 'note1', parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        const items = await (0, test_utils_synchronizer_1.allNotesFolders)();
        expect(items.length).toBe(1);
        expect(items[0].title).toBe('note1');
        expect(items[0].is_conflict).toBe(1);
    }));
    it('should resolve conflict if note has been deleted remotely and locally', (async () => {
        const folder = await Folder_1.default.save({ title: 'folder' });
        const note = await Note_1.default.save({ title: 'note', parent_id: folder.title });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        await Note_1.default.delete(note.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        await Note_1.default.delete(note.id);
        await (0, test_utils_1.synchronizerStart)();
        const items = await (0, test_utils_synchronizer_1.allNotesFolders)();
        expect(items.length).toBe(1);
        expect(items[0].title).toBe('folder');
        await (0, test_utils_synchronizer_1.localNotesFoldersSameAsRemote)(items, expect);
    }));
    it('should handle conflict when remote note is deleted then local note is modified', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        const note1 = await Note_1.default.save({ title: 'un', parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.sleep)(0.1);
        await Note_1.default.delete(note1.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        const newTitle = 'Modified after having been deleted';
        await Note_1.default.save({ id: note1.id, title: newTitle });
        await (0, test_utils_1.synchronizerStart)();
        const conflictedNotes = await Note_1.default.conflictedNotes();
        expect(conflictedNotes.length).toBe(1);
        expect(conflictedNotes[0].title).toBe(newTitle);
        const unconflictedNotes = await Note_1.default.unconflictedNotes();
        expect(unconflictedNotes.length).toBe(0);
    }));
    it('should handle conflict when remote folder is deleted then local folder is renamed', (async () => {
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        const folder2 = await Folder_1.default.save({ title: 'folder2', parent_id: folder1.id });
        const note1 = await Note_1.default.save({ title: 'note1', parent_id: folder2.id });
        const note2 = await Note_1.default.save({ title: 'note2', parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.sleep)(0.1);
        await Folder_1.default.delete(folder1.id, { deleteChildren: false });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        await (0, test_utils_1.sleep)(0.1);
        const newTitle = 'Modified after having been deleted';
        await Folder_1.default.save({ id: folder1.id, title: newTitle });
        await (0, test_utils_1.synchronizerStart)();
        const remainingFolder1 = await Folder_1.default.load(folder1.id);
        const remainingFolder2 = await Folder_1.default.load(folder2.id);
        const remainingNote1 = await Note_1.default.load(note1.id);
        const remainingNote2 = await Note_1.default.load(note2.id);
        expect(remainingFolder1).toBeUndefined();
        expect(remainingFolder2 === null || remainingFolder2 === void 0 ? void 0 : remainingFolder2.id).toBe(folder2.id);
        expect(remainingNote1 === null || remainingNote1 === void 0 ? void 0 : remainingNote1.id).toBe(note1.id);
        expect(remainingNote2 === null || remainingNote2 === void 0 ? void 0 : remainingNote2.id).toBe(note2.id);
    }));
    it('should not sync notes with conflicts', (async () => {
        const f1 = await Folder_1.default.save({ title: 'folder' });
        await Note_1.default.save({ title: 'mynote', parent_id: f1.id, is_conflict: 1 });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        const notes = await Note_1.default.all();
        const folders = await Folder_1.default.all();
        expect(notes.length).toBe(0);
        expect(folders.length).toBe(1);
    }));
    it('should not try to delete on remote conflicted notes that have been deleted', (async () => {
        const f1 = await Folder_1.default.save({ title: 'folder' });
        const n1 = await Note_1.default.save({ title: 'mynote', parent_id: f1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        await Note_1.default.save({ id: n1.id, is_conflict: 1 });
        await Note_1.default.delete(n1.id);
        const deletedItems = await BaseItem_1.default.deletedItems((0, test_utils_1.syncTargetId)());
        expect(deletedItems.length).toBe(0);
    }));
    async function ignorableNoteConflictTest(withEncryption) {
        if (withEncryption) {
            (0, syncInfoUtils_1.setEncryptionEnabled)(true);
            await (0, test_utils_1.loadEncryptionMasterKey)();
        }
        const folder1 = await Folder_1.default.save({ title: 'folder1' });
        const note1 = await Note_1.default.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(2);
        await (0, test_utils_1.synchronizerStart)();
        if (withEncryption) {
            await (0, test_utils_1.loadEncryptionMasterKey)(null, true);
            await (0, test_utils_1.decryptionWorker)().start();
        }
        let note2 = await Note_1.default.load(note1.id);
        note2.todo_completed = time_1.default.unixMs() - 1;
        await Note_1.default.save(note2);
        note2 = await Note_1.default.load(note2.id);
        await (0, test_utils_1.synchronizerStart)();
        await (0, test_utils_1.switchClient)(1);
        let note2conf = await Note_1.default.load(note1.id);
        note2conf.todo_completed = time_1.default.unixMs();
        await Note_1.default.save(note2conf);
        note2conf = await Note_1.default.load(note1.id);
        await (0, test_utils_1.synchronizerStart)();
        if (!withEncryption) {
            // That was previously a common conflict:
            // - Client 1 mark todo as "done", and sync
            // - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
            // In theory it is a conflict because the todo_completed dates are different
            // but in practice it doesn't matter, we can just take the date when the
            // todo was marked as "done" the first time.
            const conflictedNotes = await Note_1.default.conflictedNotes();
            expect(conflictedNotes.length).toBe(0);
            const notes = await Note_1.default.all();
            expect(notes.length).toBe(1);
            expect(notes[0].id).toBe(note1.id);
            expect(notes[0].todo_completed).toBe(note2.todo_completed);
        }
        else {
            // If the notes are encrypted however it's not possible to do this kind of
            // smart conflict resolving since we don't know the content, so in that
            // case it's handled as a regular conflict.
            const conflictedNotes = await Note_1.default.conflictedNotes();
            expect(conflictedNotes.length).toBe(1);
            const notes = await Note_1.default.all();
            expect(notes.length).toBe(2);
        }
    }
    it('should not consider it is a conflict if neither the title nor body of the note have changed', (async () => {
        await ignorableNoteConflictTest(false);
    }));
    it('should always handle conflict if local or remote are encrypted', (async () => {
        await ignorableNoteConflictTest(true);
    }));
});
//# sourceMappingURL=Synchronizer.conflicts.test.js.map