const time = require('../time').default;
const { setupDatabaseAndSynchronizer, switchClient, msleep } = require('../testing/test-utils.js');
const Folder = require('../models/Folder').default;
const Note = require('../models/Note').default;

describe('models/Note_CustomSortOrder', () => {
	beforeEach(async () => {
		await setupDatabaseAndSynchronizer(1);
		await switchClient(1);
	});

	it('should set the order property when saving a note', (async () => {
		const now = Date.now();
		const n1 = await Note.save({ title: 'testing' });
		expect(n1.order).toBeGreaterThanOrEqual(now);

		const n2 = await Note.save({ title: 'testing', order: 0 });
		expect(n2.order).toBe(0);
	}));

	it('should insert notes at the specified position (order 0)', (async () => {
		// Notes always had an "order" property, but for a long time it wasn't used, and
		// set to 0. For custom sorting to work though, it needs to be set to some number
		// (which normally is the creation timestamp). So if the user tries to move notes
		// in the middle of other notes with order = 0, the order of all these notes is
		// initialised at this point.
		// Also check that the order-0 notes stay in their previous position when their
		// order values are initialized. There was at one point a bug where they popped to
		// the top.

		const folder1 = await Folder.save({});
		const folder2 = await Folder.save({});

		const notes1 = [];
		notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
		notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
		notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
		notes1.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		const notes2 = [];
		notes2.push(await Note.save({ parent_id: folder2.id })); await time.msleep(2);
		notes2.push(await Note.save({ parent_id: folder2.id })); await time.msleep(2);

		const originalTimestamps = {};
		for (const n of notes1) {
			originalTimestamps[n.id] = {
				user_created_time: n.user_created_time,
				user_updated_time: n.user_updated_time,
			};
		}

		await Note.insertNotesAt(folder1.id, notes2.map(n => n.id), 2);

		const newNotes1 = [
			await Note.load(notes1[0].id),
			await Note.load(notes1[1].id),
			await Note.load(notes1[2].id),
			await Note.load(notes1[3].id),
		];

		// Check that timestamps haven't changed - moving a note should not change the user timestamps
		for (let i = 0; i < newNotes1.length; i++) {
			const n = newNotes1[i];
			expect(n.user_created_time).toBe(originalTimestamps[n.id].user_created_time);
			expect(n.user_updated_time).toBe(originalTimestamps[n.id].user_updated_time);
		}

		const sortedNotes = await Note.previews(folder1.id, {
			order: Note.customOrderByColumns(),
		});

		expect(sortedNotes.length).toBe(6);
		expect(sortedNotes[0].id).toBe(notes1[3].id);
		expect(sortedNotes[1].id).toBe(notes1[2].id);
		expect(sortedNotes[2].id).toBe(notes2[0].id);
		expect(sortedNotes[3].id).toBe(notes2[1].id);
		expect(sortedNotes[4].id).toBe(notes1[1].id);
		expect(sortedNotes[5].id).toBe(notes1[0].id);
	}));

	it('should bump system but not user updated time when changing sort value', (async () => {
		const folder1 = await Folder.save({ title: 'Folder' });
		const note0 = await Note.save({ title: 'A3', parent_id: folder1.id, is_todo: 0, order: 3 });
		const note1 = await Note.save({ title: 'A20', parent_id: folder1.id, is_todo: 0, order: 2 });
		const note2 = await Note.save({ title: 'A100', parent_id: folder1.id, is_todo: 0, order: 1 });

		const sortedNotes1 = await Note.previews(folder1.id, {
			fields: ['id', 'title'],
			order: Note.customOrderByColumns(),
		});
		expect(sortedNotes1.length).toBe(3);
		expect(sortedNotes1[0].id).toBe(note0.id);
		expect(sortedNotes1[1].id).toBe(note1.id);
		expect(sortedNotes1[2].id).toBe(note2.id);

		const timeBefore = time.unixMs();

		await msleep(10);

		await Note.insertNotesAt(folder1.id, [note2.id], 0);
		await Note.insertNotesAt(folder1.id, [note1.id], 1);

		const sortedNotes2 = await Note.previews(folder1.id, {
			fields: ['id', 'title', 'updated_time', 'user_updated_time'],
			order: Note.customOrderByColumns(),
		});
		expect(sortedNotes2.length).toBe(3);
		expect(sortedNotes2[0].id).toBe(note2.id);
		expect(sortedNotes2[1].id).toBe(note1.id);
		expect(sortedNotes2[2].id).toBe(note0.id);

		expect(sortedNotes2[0].updated_time).toBeGreaterThan(timeBefore);
		expect(sortedNotes2[1].updated_time).toBeGreaterThan(timeBefore);
		expect(sortedNotes2[2].updated_time).toBeLessThan(timeBefore);

		expect(sortedNotes2[0].user_updated_time).toBeLessThan(timeBefore);
		expect(sortedNotes2[1].user_updated_time).toBeLessThan(timeBefore);
		expect(sortedNotes2[2].user_updated_time).toBeLessThan(timeBefore);
	}));

	it('should insert notes at the specified position (targets with same orders)', (async () => {
		// If the target notes all have the same order, inserting a note should work
		// anyway, because the order of the other notes will be updated as needed.

		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		await Note.insertNotesAt(folder1.id, [notes[0].id], 1);

		const sortedNotes = await Note.previews(folder1.id, {
			order: Note.customOrderByColumns(),
		});

		expect(sortedNotes.length).toBe(4);
		expect(sortedNotes[0].id).toBe(notes[3].id);
		expect(sortedNotes[1].id).toBe(notes[0].id);
		expect(sortedNotes[2].id).toBe(notes[2].id);
		expect(sortedNotes[3].id).toBe(notes[1].id);
	}));

	it('should insert notes at the specified position (insert at end)', (async () => {
		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		await Note.insertNotesAt(folder1.id, [notes[1].id], 4);

		const sortedNotes = await Note.previews(folder1.id, {
			fields: ['id', 'order', 'user_created_time'],
			order: Note.customOrderByColumns(),
		});

		expect(sortedNotes.length).toBe(4);
		expect(sortedNotes[0].id).toBe(notes[3].id);
		expect(sortedNotes[1].id).toBe(notes[2].id);
		expect(sortedNotes[2].id).toBe(notes[0].id);
		expect(sortedNotes[3].id).toBe(notes[1].id);
	}));

	it('should insert notes at the specified position (insert at beginning)', (async () => {
		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		await Note.insertNotesAt(folder1.id, [notes[2].id], 0);

		const sortedNotes = await Note.previews(folder1.id, {
			fields: ['id', 'order', 'user_created_time'],
			order: Note.customOrderByColumns(),
		});

		expect(sortedNotes.length).toBe(4);
		expect(sortedNotes[0].id).toBe(notes[2].id);
		expect(sortedNotes[1].id).toBe(notes[3].id);
		expect(sortedNotes[2].id).toBe(notes[1].id);
		expect(sortedNotes[3].id).toBe(notes[0].id);
	}));

	it('should insert notes even if sources are not adjacent', (async () => {
		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		await Note.insertNotesAt(folder1.id, [notes[1].id, notes[3].id], 0);

		const sortedNotes = await Note.previews(folder1.id, {
			fields: ['id', 'order', 'user_created_time'],
			order: Note.customOrderByColumns(),
		});

		expect(sortedNotes.length).toBe(4);
		expect(sortedNotes[0].id).toBe(notes[1].id);
		expect(sortedNotes[1].id).toBe(notes[3].id);
		expect(sortedNotes[2].id).toBe(notes[2].id);
		expect(sortedNotes[3].id).toBe(notes[0].id);
	}));

	it('should account for completion-hidden and uncompleted-on-top todos', (async () => {
		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1006, parent_id: folder1.id, is_todo: true, todo_completed: time.unixMs() })); await time.msleep(2);
		notes.push(await Note.save({ order: 1005, parent_id: folder1.id, is_todo: true })); await time.msleep(2);
		notes.push(await Note.save({ order: 1004, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1003, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1002, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1001, parent_id: folder1.id, is_todo: true })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id, is_todo: true })); await time.msleep(2);

		const sortNotes = async () => await Note.previews(folder1.id, {
			fields: ['id', 'order', 'user_created_time', 'is_todo', 'todo_completed'],
			order: Note.customOrderByColumns(),
			uncompletedTodosOnTop: true,
			showCompletedTodos: false,
		});

		// Initial sort is as expected in UI, with completed item missing, and uncompleted
		// todos at top.
		const initialSortedNotes = await sortNotes();
		expect(initialSortedNotes.length).toBe(6);
		expect(initialSortedNotes[0].id).toBe(notes[1].id);
		expect(initialSortedNotes[1].id).toBe(notes[5].id);
		expect(initialSortedNotes[2].id).toBe(notes[6].id);
		expect(initialSortedNotes[3].id).toBe(notes[2].id);
		expect(initialSortedNotes[4].id).toBe(notes[3].id);
		expect(initialSortedNotes[5].id).toBe(notes[4].id);

		// Regular reorder - place a note within its window
		await Note.insertNotesAt(folder1.id, [initialSortedNotes[5].id], 4, true, false);

		// Post-reorder, the updated note is at the index requested
		const resortedNotes1 = await sortNotes();
		expect(resortedNotes1.length).toBe(6);
		expect(resortedNotes1[0].id).toBe(initialSortedNotes[0].id);
		expect(resortedNotes1[1].id).toBe(initialSortedNotes[1].id);
		expect(resortedNotes1[2].id).toBe(initialSortedNotes[2].id);
		expect(resortedNotes1[3].id).toBe(initialSortedNotes[3].id);
		expect(resortedNotes1[4].id).toBe(initialSortedNotes[5].id);
		expect(resortedNotes1[5].id).toBe(initialSortedNotes[4].id);

		// Limit reorder - place the note into another window
		await Note.insertNotesAt(folder1.id, [resortedNotes1[4].id], 2, true, false);

		// Post-reorder, the updated note is not at the index requested, but
		// as close as possible: at the top edge of its window.
		const resortedNotes2 = await sortNotes();
		expect(resortedNotes2.length).toBe(6);
		expect(resortedNotes2[0].id).toBe(resortedNotes1[0].id);
		expect(resortedNotes2[1].id).toBe(resortedNotes1[1].id);
		expect(resortedNotes2[2].id).toBe(resortedNotes1[2].id);
		expect(resortedNotes2[3].id).toBe(resortedNotes1[4].id);
		expect(resortedNotes2[4].id).toBe(resortedNotes1[3].id);
		expect(resortedNotes2[5].id).toBe(resortedNotes1[5].id);

		// Limit reorder - place an uncompleted todo into another window
		await Note.insertNotesAt(folder1.id, [resortedNotes2[0].id], 4, true, false);

		// Post-reorder, the updated todo is not at the index requested, but
		// as close as possible: at the lower edge of its window.
		const resortedNotes3 = await sortNotes();
		expect(resortedNotes3.length).toBe(6);
		expect(resortedNotes3[0].id).toBe(resortedNotes2[1].id);
		expect(resortedNotes3[1].id).toBe(resortedNotes2[2].id);
		expect(resortedNotes3[2].id).toBe(resortedNotes2[0].id);
		expect(resortedNotes3[3].id).toBe(resortedNotes2[3].id);
		expect(resortedNotes3[4].id).toBe(resortedNotes2[4].id);
		expect(resortedNotes3[5].id).toBe(resortedNotes2[5].id);
	}));

	it('should account for items in the trash', async () => {
		const folder1 = await Folder.save({});

		const notes = [];
		notes.push(await Note.save({ order: 1003, parent_id: folder1.id, deleted_time: 1 })); await time.msleep(2);
		notes.push(await Note.save({ order: 1002, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1001, parent_id: folder1.id })); await time.msleep(2);
		notes.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);

		const sortedNoteIds = async () => {
			const notes = await Note.previews(folder1.id, {
				fields: ['id', 'order', 'user_created_time', 'is_todo', 'todo_completed'],
				order: Note.customOrderByColumns(),
			});
			return notes.map(note => note.id);
		};

		// Should sort items correctly initially, with deleted items omitted
		expect(await sortedNoteIds()).toEqual([
			notes[1].id, notes[2].id, notes[3].id,
		]);

		// Move a note to the end
		await Note.insertNotesAt(folder1.id, [notes[1].id], 3, true, true);

		// Should correctly reorder notes
		expect(await sortedNoteIds()).toEqual([
			notes[2].id, notes[3].id, notes[1].id,
		]);
	});
});
