/* @typescript-eslint/prefer-const */

import time from '../../time';
import { setupDatabaseAndSynchronizer, supportDir, db, createNTestNotes, switchClient } from '../../testing/test-utils';
import SearchEngine from './SearchEngine';
import Note from '../../models/Note';
import Folder from '../../models/Folder';
import Tag from '../../models/Tag';
import shim from '../../shim';
import ResourceService from '../ResourceService';
import { NoteEntity } from '../database/types';


// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let engine: any = null;

const ids = (array: NoteEntity[]) => array.map(a => a.id);

const dateStringToTimestamp = (dateString: string) => {
	const localTimestamp = new Date(dateString);

	// Without getTimezoneOffset(), .getTime() doesn't account for the timezone offset.
	const minutesToMilliseconds = 1000 * 60;
	return localTimestamp.getTime() + localTimestamp.getTimezoneOffset() * minutesToMilliseconds;
};

describe('services_SearchFilter', () => {
	beforeEach(async () => {
		await setupDatabaseAndSynchronizer(1);
		await switchClient(1);

		engine = new SearchEngine();
		engine.setDb(db());
	});

	// Outside of for loop because this does not apply to to SEARCH_TYPE_NONLATIN_SCRIPT
	it('should ignore dashes in a word', (async () => {
		const n0 = await Note.save({ title: 'doesnotwork' });
		const n1 = await Note.save({ title: 'does not work' });
		const n2 = await Note.save({ title: 'does-not-work' });
		const n3 = await Note.save({ title: 'does_not_work' });

		await engine.syncTables();

		let rows = await engine.search('does-not-work');
		expect(rows.length).toBe(3);
		expect(ids(rows)).toContain(n1.id);
		expect(ids(rows)).toContain(n2.id);
		expect(ids(rows)).toContain(n3.id);

		rows = await engine.search('does not work');
		expect(rows.length).toBe(3);
		expect(ids(rows)).toContain(n1.id);
		expect(ids(rows)).toContain(n2.id);
		expect(ids(rows)).toContain(n3.id);

		rows = await engine.search('"does not work"');
		expect(rows.length).toBe(3);
		expect(ids(rows)).toContain(n1.id);
		expect(ids(rows)).toContain(n2.id);
		expect(ids(rows)).toContain(n3.id);

		rows = await engine.search('title:does-not-work');
		expect(rows.length).toBe(3);
		expect(ids(rows)).toContain(n1.id);
		expect(ids(rows)).toContain(n2.id);
		expect(ids(rows)).toContain(n3.id);

		rows = await engine.search('doesnotwork');
		expect(rows.length).toBe(1);
		expect(ids(rows)).toContain(n0.id);

	}));

	for (const searchType of [SearchEngine.SEARCH_TYPE_FTS, SearchEngine.SEARCH_TYPE_NONLATIN_SCRIPT]) {

		describe(`search type ${searchType}`, () => {
			it('check case insensitivity for filter keywords', (async () => {
				let rows;
				const notebook1 = await Folder.save({ title: 'folderA' });
				const notebook2 = await Folder.save({ title: 'folderB' });
				const note1 = await Note.save({ title: 'Note1', body: 'obelix', parent_id: notebook1.id });
				const note2 = await Note.save({ title: 'Note2', body: 'asterix', parent_id: notebook2.id });
				const note3 = await Note.save({ title: 'Note3', body: 'rom', parent_id: notebook1.id });

				await Tag.setNoteTagsByTitles(note1.id, ['tag1', 'tag2']);
				await Tag.setNoteTagsByTitles(note2.id, ['tag2', 'tag3']);
				await Tag.setNoteTagsByTitles(note3.id, ['tag3', 'tag4', 'space travel']);

				await engine.syncTables();

				const testCases = [
					{ searchString: 'tag:tag2', expectedResults: 2, expectedNoteIds: [note1.id, note2.id] },
					{ searchString: 'tAg:tag2', expectedResults: 2, expectedNoteIds: [note1.id, note2.id] },
					{ searchString: 'Tag:tag2', expectedResults: 2, expectedNoteIds: [note1.id, note2.id] },
					{ searchString: '-tag:tag2', expectedResults: 1, expectedNoteIds: [note3.id] },
					{ searchString: '-Tag:tag2', expectedResults: 1, expectedNoteIds: [note3.id] },
					{ searchString: 'title:Note1', expectedResults: 1, expectedNoteIds: [note1.id] },
					{ searchString: 'Title:Note1', expectedResults: 1, expectedNoteIds: [note1.id] },
					{ searchString: 'Any:1 -tag:tag1 -notebook:folderB', expectedResults: 1, expectedNoteIds: [note3.id] },
					{ searchString: 'notebook:folderA', expectedResults: 2, expectedNoteIds: [note1.id, note3.id] },
					{ searchString: 'notebooK:folderA', expectedResults: 2, expectedNoteIds: [note1.id, note3.id] },
				];

				for (const testCase of testCases) {
					rows = await engine.search(testCase.searchString, { searchType });
					expect(rows.length).toBe(testCase.expectedResults);
					for (const expectedNoteId of testCase.expectedNoteIds) {
						expect(ids(rows)).toContain(expectedNoteId);
					}
				}
			}));

			it('should return note matching title', (async () => {
				const n1 = await Note.save({ title: 'abcd', body: 'body 1' });
				await Note.save({ title: 'efgh', body: 'body 2' });

				await engine.syncTables();
				const rows = await engine.search('title: abcd', { searchType });

				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n1.id);
			}));

			it('should return note matching negated title', (async () => {
				await Note.save({ title: 'abcd', body: 'body 1' });
				const n2 = await Note.save({ title: 'efgh', body: 'body 2' });

				await engine.syncTables();
				const rows = await engine.search('-title: abcd', { searchType });

				expect(rows.length).toBe(1);

				expect(rows[0].id).toBe(n2.id);
			}));

			it('should return note matching body', (async () => {
				const n1 = await Note.save({ title: 'abcd', body: 'body1' });
				await Note.save({ title: 'efgh', body: 'body2' });

				await engine.syncTables();
				const rows = await engine.search('body: body1', { searchType });

				expect(rows.length).toBe(1);

				expect(rows[0].id).toBe(n1.id);
			}));

			it('should return note matching negated body', (async () => {
				await Note.save({ title: 'abcd', body: 'body1' });
				const n2 = await Note.save({ title: 'efgh', body: 'body2' });

				await engine.syncTables();
				const rows = await engine.search('-body: body1', { searchType });

				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n2.id);
			}));

			it('should return note matching title containing multiple words', (async () => {
				const n1 = await Note.save({ title: 'abcd xyz', body: 'body1' });
				await Note.save({ title: 'efgh ijk', body: 'body2' });

				await engine.syncTables();
				const rows = await engine.search('title: "abcd xyz"', { searchType });

				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n1.id);
			}));

			it('should return note matching body containing multiple words', (async () => {
				await Note.save({ title: 'abcd', body: 'ho ho ho' });
				const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });

				await engine.syncTables();
				const rows = await engine.search('body: "foo bar"', { searchType });

				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n2.id);
			}));

			it('should return note matching title AND body', (async () => {
				let rows;
				await Note.save({ title: 'abcd', body: 'ho ho ho' });
				const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });

				await engine.syncTables();
				rows = await engine.search('title: efgh body: "foo bar"', { searchType });
				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n2.id);

				rows = await engine.search('title: abcd body: "foo bar"', { searchType });
				expect(rows.length).toBe(0);
			}));

			it('should return note matching title OR body', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'abcd', body: 'ho ho ho' });
				const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });

				await engine.syncTables();
				rows = await engine.search('any:1 title: abcd body: "foo bar"', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('any:1 title: wxyz body: "blah blah"', { searchType });
				expect(rows.length).toBe(0);
			}));

			it('should return notes matching text', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'foo beef', body: 'dead bar' });
				const n2 = await Note.save({ title: 'bar efgh', body: 'foo dog' });
				const n3 = await Note.save({ title: 'foo ho', body: 'ho ho ho' });
				await engine.syncTables();

				// Interpretation: Match with notes containing foo in title/body and bar in title/body
				// Note: This is NOT saying to match notes containing foo bar in title/body
				rows = await engine.search('foo bar', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('foo -bar', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('foo efgh', { searchType });
				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n2.id);

				rows = await engine.search('zebra', { searchType });
				expect(rows.length).toBe(0);
			}));

			it('should return notes matching any negated text', (async () => {
				const n1 = await Note.save({ title: 'abc', body: 'def' });
				const n2 = await Note.save({ title: 'def', body: 'ghi' });
				const n3 = await Note.save({ title: 'ghi', body: 'jkl' });
				await engine.syncTables();

				const rows = await engine.search('any:1 -abc -ghi', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should return notes matching any negated title', (async () => {
				const n1 = await Note.save({ title: 'abc', body: 'def' });
				const n2 = await Note.save({ title: 'def', body: 'ghi' });
				const n3 = await Note.save({ title: 'ghi', body: 'jkl' });
				await engine.syncTables();

				const rows = await engine.search('any:1 -title:abc -title:ghi', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should return notes matching any negated body', (async () => {
				const n1 = await Note.save({ title: 'abc', body: 'def' });
				const n2 = await Note.save({ title: 'def', body: 'ghi' });
				const n3 = await Note.save({ title: 'ghi', body: 'jkl' });
				await engine.syncTables();

				const rows = await engine.search('any:1 -body:xyz -body:ghi', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support phrase search', (async () => {
				const n1 = await Note.save({ title: 'foo beef', body: 'bar dog' });
				await Note.save({ title: 'bar efgh', body: 'foo dog' });
				await engine.syncTables();

				const rows = await engine.search('"bar dog"', { searchType });
				expect(rows.length).toBe(1);
				expect(rows[0].id).toBe(n1.id);
			}));

			it('should support prefix search', (async () => {
				const n1 = await Note.save({ title: 'foo beef', body: 'bar dog' });
				const n2 = await Note.save({ title: 'bar efgh', body: 'foo dog' });
				await engine.syncTables();

				const rows = await engine.search('"bar*"', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
			}));

			it('should support filtering by tags', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'But I would', body: 'walk 500 miles' });
				const n2 = await Note.save({ title: 'And I would', body: 'walk 500 more' });
				const n3 = await Note.save({ title: 'Just to be', body: 'the man who' });
				const n4 = await Note.save({ title: 'walked a thousand', body: 'miles to fall' });
				const n5 = await Note.save({ title: 'down at your', body: 'door' });

				await Tag.setNoteTagsByTitles(n1.id, ['Da', 'da', 'lat', 'da']);
				await Tag.setNoteTagsByTitles(n2.id, ['Da', 'da', 'lat', 'da']);

				await engine.syncTables();

				rows = await engine.search('tag:*', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('-tag:*', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n3.id);
				expect(ids(rows)).toContain(n4.id);
				expect(ids(rows)).toContain(n5.id);
			}));


			it('should support filtering by tags (2)', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'peace talks', body: 'battle ground' });
				const n2 = await Note.save({ title: 'mouse', body: 'mister' });
				const n3 = await Note.save({ title: 'dresden files', body: 'harry dresden' });

				await Tag.setNoteTagsByTitles(n1.id, ['tag1', 'tag2']);
				await Tag.setNoteTagsByTitles(n2.id, ['tag2', 'tag3']);
				await Tag.setNoteTagsByTitles(n3.id, ['tag3', 'tag4', 'space travel']);

				await engine.syncTables();

				rows = await engine.search('tag:tag2', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('tag:tag2 tag:tag3', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('any:1 tag:tag1 tag:tag2 tag:tag3', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('tag:tag2 tag:tag3 tag:tag4', { searchType });
				expect(rows.length).toBe(0);

				rows = await engine.search('-tag:tag2', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('-tag:tag2 -tag:tag3', { searchType });
				expect(rows.length).toBe(0);

				rows = await engine.search('-tag:tag2 -tag:tag3', { searchType });
				expect(rows.length).toBe(0);

				rows = await engine.search('any:1 -tag:tag2 -tag:tag3', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('tag:"space travel"', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by notebook', (async () => {
				const folder0 = await Folder.save({ title: 'notebook0' });
				const folder1 = await Folder.save({ title: 'notebook1' });
				const notes0 = await createNTestNotes(5, folder0);
				await createNTestNotes(5, folder1);

				await engine.syncTables();

				const rows = await engine.search('notebook:notebook0', { searchType });
				expect(rows.length).toBe(5);
				expect(ids(rows).sort()).toEqual(ids(notes0).sort());

			}));

			it('should support filtering by nested notebook', (async () => {
				const folder0 = await Folder.save({ title: 'notebook0' });
				const folder00 = await Folder.save({ title: 'notebook00', parent_id: folder0.id });
				const folder1 = await Folder.save({ title: 'notebook1' });
				const notes0 = await createNTestNotes(5, folder0);
				const notes00 = await createNTestNotes(5, folder00);
				await createNTestNotes(5, folder1);

				await engine.syncTables();

				const rows = await engine.search('notebook:notebook0', { searchType });
				expect(rows.length).toBe(10);
				expect(ids(rows).sort()).toEqual(ids(notes0.concat(notes00)).sort());
			}));

			it('should support filtering by multiple notebooks', (async () => {
				const folder0 = await Folder.save({ title: 'notebook0' });
				const folder00 = await Folder.save({ title: 'notebook00', parent_id: folder0.id });
				const folder1 = await Folder.save({ title: 'notebook1' });
				const folder2 = await Folder.save({ title: 'notebook2' });
				const notes0 = await createNTestNotes(5, folder0);
				const notes00 = await createNTestNotes(5, folder00);
				const notes1 = await createNTestNotes(5, folder1);
				await createNTestNotes(5, folder2);

				await engine.syncTables();

				const rows = await engine.search('notebook:notebook0 notebook:notebook1', { searchType });
				expect(rows.length).toBe(15);
				expect(ids(rows).sort()).toEqual(ids(notes0).concat(ids(notes00).concat(ids(notes1))).sort());
			}));

			it('should support filtering and search term', (async () => {
				const notebook1 = await Folder.save({ title: 'notebook1' });
				const notebook2 = await Folder.save({ title: 'notebook2' });
				const note1 = await Note.save({ title: 'note1', body: 'abcdefg', parent_id: notebook1.id });
				await Note.save({ title: 'note2', body: 'body', parent_id: notebook1.id });
				await Note.save({ title: 'note3', body: 'abcdefg', parent_id: notebook2.id });
				await Note.save({ title: 'note4', body: 'body', parent_id: notebook2.id });

				await engine.syncTables();

				const testCases = [
					{ searchQuery: 'notebook:notebook1 abcdefg' },
					{ searchQuery: 'notebook:notebook1 "abcdefg"' },
					{ searchQuery: 'notebook:"notebook1" abcdefg' },
					{ searchQuery: 'notebook:"notebook1" "abcdefg"' },
					{ searchQuery: 'notebook:"notebook1" -tag:* "abcdefg"' },
					{ searchQuery: 'notebook:"notebook1" -tag:* abcdefg' },
					{ searchQuery: 'notebook:"notebook1" -tag:"*" abcdefg' },
					{ searchQuery: 'notebook:"notebook1" -tag:"*" "abcdefg"' },
				];

				for (const testCase of testCases) {
					const rows = await engine.search(testCase.searchQuery, { searchType });
					expect(rows.length).toBe(1);
					expect(ids(rows)).toContain(note1.id);
				}
			}));

			it('should support filtering by created date', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this on', body: 'May 20 2020', user_created_time: dateStringToTimestamp('2020-05-20') });
				const n2 = await Note.save({ title: 'I made this on', body: 'May 19 2020', user_created_time: dateStringToTimestamp('2020-05-19') });
				const n3 = await Note.save({ title: 'I made this on', body: 'May 18 2020', user_created_time: dateStringToTimestamp('2020-05-18') });

				await engine.syncTables();

				rows = await engine.search('created:20200520', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:20200519', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('-created:20200519', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n3.id);

			}));

			it('should support filtering by between two dates', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'January 01 2020', body: 'January 01 2020', user_created_time: dateStringToTimestamp('2020-01-01') });
				const n2 = await Note.save({ title: 'February 15 2020', body: 'February 15 2020', user_created_time: dateStringToTimestamp('2020-02-15') });
				const n3 = await Note.save({ title: 'March 25 2019', body: 'March 25 2019', user_created_time: dateStringToTimestamp('2019-03-25') });
				const n4 = await Note.save({ title: 'March 01 2018', body: 'March 01 2018', user_created_time: dateStringToTimestamp('2018-03-01') });

				await engine.syncTables();

				rows = await engine.search('created:2018 -created:2019', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n4.id);

				rows = await engine.search('created:201901 -created:202002', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n3.id);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:20200101 -created:20200220', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
			}));

			it('should support filtering by created with smart value: day', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this', body: 'today', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'day'), 10) });
				const n2 = await Note.save({ title: 'I made this', body: 'yesterday', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'day'), 10) });
				const n3 = await Note.save({ title: 'I made this', body: 'day before yesterday', user_created_time: parseInt(time.goBackInTime(Date.now(), 2, 'day'), 10) });

				await engine.syncTables();

				rows = await engine.search('created:day-0', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:day-1', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('created:day-2', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by created with smart value: week', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this', body: 'this week', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'week'), 10) });
				const n2 = await Note.save({ title: 'I made this', body: 'the week before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'week'), 10) });
				const n3 = await Note.save({ title: 'I made this', body: 'before before week', user_created_time: parseInt(time.goBackInTime(Date.now(), 2, 'week'), 10) });

				await engine.syncTables();

				rows = await engine.search('created:week-0', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:week-1', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('created:week-2', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by created with smart value: month', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this', body: 'this month', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'month'), 10) });
				const n2 = await Note.save({ title: 'I made this', body: 'the month before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'month'), 10) });
				const n3 = await Note.save({ title: 'I made this', body: 'before before month', user_created_time: parseInt(time.goBackInTime(Date.now(), 2, 'month'), 10) });

				await engine.syncTables();

				rows = await engine.search('created:month-0', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:month-1', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('created:month-2', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by created with smart value: year', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this', body: 'this year', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'year'), 10) });
				const n2 = await Note.save({ title: 'I made this', body: 'the year before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'year'), 10) });
				const n3 = await Note.save({ title: 'I made this', body: 'before before year', user_created_time: parseInt(time.goBackInTime(Date.now(), 2, 'year'), 10) });

				await engine.syncTables();

				rows = await engine.search('created:year-0', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('created:year-1', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('created:year-2', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by updated date', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I updated this on', body: 'May 20 2020', updated_time: dateStringToTimestamp('2020-05-20'), user_updated_time: dateStringToTimestamp('2020-05-20') }, { autoTimestamp: false });
				const n2 = await Note.save({ title: 'I updated this on', body: 'May 19 2020', updated_time: dateStringToTimestamp('2020-05-19'), user_updated_time: dateStringToTimestamp('2020-05-19') }, { autoTimestamp: false });

				await engine.syncTables();

				rows = await engine.search('updated:20200520', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('updated:20200519', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
			}));

			it('should support filtering by updated with smart value: day', (async () => {
				let rows;
				const today = parseInt(time.goBackInTime(Date.now(), 0, 'day'), 10);
				const yesterday = parseInt(time.goBackInTime(Date.now(), 1, 'day'), 10);
				const dayBeforeYesterday = parseInt(time.goBackInTime(Date.now(), 2, 'day'), 10);
				const n1 = await Note.save({ title: 'I made this', body: 'today', updated_time: today, user_updated_time: today }, { autoTimestamp: false });
				const n11 = await Note.save({ title: 'I also made this', body: 'today', updated_time: today, user_updated_time: today }, { autoTimestamp: false });

				const n2 = await Note.save({ title: 'I made this', body: 'yesterday', updated_time: yesterday, user_updated_time: yesterday }, { autoTimestamp: false });
				const n3 = await Note.save({ title: 'I made this', body: 'day before yesterday', updated_time: dayBeforeYesterday, user_updated_time: dayBeforeYesterday }, { autoTimestamp: false });

				await engine.syncTables();

				rows = await engine.search('updated:day-0', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n11.id);

				rows = await engine.search('updated:day-1', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n11.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('updated:day-2', { searchType });
				expect(rows.length).toBe(4);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n11.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by type todo', (async () => {
				let rows;
				const t1 = await Note.save({ title: 'This is a ', body: 'todo', is_todo: 1 });
				const t2 = await Note.save({ title: 'This is another', body: 'todo but completed', is_todo: 1, todo_completed: 1590085027710 });
				await Note.save({ title: 'This is NOT a ', body: 'todo' });

				await engine.syncTables();

				rows = await engine.search('type:todo', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(t1.id);
				expect(ids(rows)).toContain(t2.id);

				rows = await engine.search('any:1 type:todo', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(t1.id);
				expect(ids(rows)).toContain(t2.id);

				rows = await engine.search('iscompleted:1', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(t2.id);

				rows = await engine.search('iscompleted:0', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(t1.id);
			}));

			it('should support filtering by type note', (async () => {
				await Note.save({ title: 'This is a ', body: 'todo', is_todo: 1 });
				await Note.save({ title: 'This is another', body: 'todo but completed', is_todo: 1, todo_completed: 1590085027710 });
				const t3 = await Note.save({ title: 'This is NOT a ', body: 'todo' });

				await engine.syncTables();

				const rows = await engine.search('type:note', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(t3.id);
			}));

			it('should support filtering by due date', (async () => {
				let rows;
				const toDo1 = await Note.save({ title: 'ToDo 1', body: 'todo', is_todo: 1, todo_due: dateStringToTimestamp('2021-04-27') });
				const toDo2 = await Note.save({ title: 'ToDo 2', body: 'todo', is_todo: 1, todo_due: dateStringToTimestamp('2021-03-17') });
				await Note.save({ title: 'Note 1', body: 'Note' });

				await engine.syncTables();

				rows = await engine.search('due:20210425', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(toDo1.id);

				rows = await engine.search('-due:20210425', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(toDo2.id);
			}));

			it('should support filtering by due with smart value: day', (async () => {
				let rows;

				const inThreeDays = parseInt(time.goForwardInTime(Date.now(), 3, 'day'), 10);
				const inSevenDays = parseInt(time.goForwardInTime(Date.now(), 7, 'day'), 10);
				const threeDaysAgo = parseInt(time.goBackInTime(Date.now(), 3, 'day'), 10);
				const sevenDaysAgo = parseInt(time.goBackInTime(Date.now(), 7, 'day'), 10);

				const toDo1 = await Note.save({ title: 'ToDo + 3 day', body: 'toto', is_todo: 1, todo_due: inThreeDays });
				const toDo2 = await Note.save({ title: 'ToDo + 7 day', body: 'toto', is_todo: 1, todo_due: inSevenDays });
				const toDo3 = await Note.save({ title: 'ToDo - 3 day', body: 'toto', is_todo: 1, todo_due: threeDaysAgo });
				const toDo4 = await Note.save({ title: 'ToDo - 7 day', body: 'toto', is_todo: 1, todo_due: sevenDaysAgo });

				await engine.syncTables();

				rows = await engine.search('due:day-4', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(toDo1.id);
				expect(ids(rows)).toContain(toDo2.id);
				expect(ids(rows)).toContain(toDo3.id);

				rows = await engine.search('-due:day-4', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(toDo4.id);

				rows = await engine.search('-due:day+4', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(toDo1.id);
				expect(ids(rows)).toContain(toDo3.id);
				expect(ids(rows)).toContain(toDo4.id);

				rows = await engine.search('due:day+4', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(toDo2.id);

				rows = await engine.search('due:day-4 -due:day+4', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(toDo1.id);
				expect(ids(rows)).toContain(toDo3.id);
			}));

			it('should support filtering by latitude, longitude, altitude', (async () => {
				let rows;
				const n1 = await Note.save({ title: 'I made this', body: 'this week', latitude: 12.97, longitude: 88.88, altitude: 69.96 });
				const n2 = await Note.save({ title: 'I made this', body: 'the week before', latitude: 42.11, longitude: 77.77, altitude: 42.00 });
				const n3 = await Note.save({ title: 'I made this', body: 'before before week', latitude: 82.01, longitude: 66.66, altitude: 13.13 });

				await engine.syncTables();

				rows = await engine.search('latitude:13.5', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('-latitude:40', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('latitude:13 -latitude:80', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('altitude:13.5', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('-altitude:80.12', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('longitude:70 -longitude:80', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('latitude:20 longitude:50 altitude:40', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('any:1 latitude:20 longitude:50 altitude:40', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
			}));

			it('should support filtering by resource MIME type', (async () => {
				let rows;
				const service = new ResourceService();
				// console.log(testImagePath)
				const folder1 = await Folder.save({ title: 'folder1' });
				let n1 = await Note.save({ title: 'I have a picture', body: 'Im awesome', parent_id: folder1.id });
				const n2 = await Note.save({ title: 'Boring note 1', body: 'I just have text', parent_id: folder1.id });
				const n3 = await Note.save({ title: 'Boring note 2', body: 'me too', parent_id: folder1.id });
				let n4 = await Note.save({ title: 'A picture?', body: 'pfff, I have a pdf', parent_id: folder1.id });
				await engine.syncTables();

				// let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
				n1 = await shim.attachFileToNote(n1, `${supportDir}/photo.jpg`);
				// const resource1 = (await Resource.all())[0];

				n4 = await shim.attachFileToNote(n4, `${supportDir}/welcome.pdf`);

				await service.indexNoteResources();

				rows = await engine.search('resource:image/jpeg', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('resource:image/*', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('resource:application/pdf', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n4.id);

				rows = await engine.search('-resource:image/jpeg', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);
				expect(ids(rows)).toContain(n4.id);

				rows = await engine.search('any:1 resource:application/pdf resource:image/jpeg', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n4.id);
			}));


			it('should support filtering by sourceurl', (async () => {
				const n0 = await Note.save({ title: 'n0', source_url: 'https://discourse.joplinapp.org' });
				const n1 = await Note.save({ title: 'n1', source_url: 'https://google.com' });
				const n2 = await Note.save({ title: 'n2', source_url: 'https://reddit.com' });
				const n3 = await Note.save({ title: 'n3', source_url: 'https://joplinapp.org' });

				await engine.syncTables();

				let rows = await engine.search('sourceurl:https://joplinapp.org', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('sourceurl:https://google.com', { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(n1.id);

				rows = await engine.search('any:1 sourceurl:https://google.com sourceurl:https://reddit.com', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

				rows = await engine.search('-sourceurl:https://google.com', { searchType });
				expect(rows.length).toBe(3);
				expect(ids(rows)).toContain(n0.id);
				expect(ids(rows)).toContain(n2.id);
				expect(ids(rows)).toContain(n3.id);

				rows = await engine.search('sourceurl:*joplinapp.org', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n0.id);
				expect(ids(rows)).toContain(n3.id);

			}));

			it('should support negating notebooks', (async () => {

				const folder1 = await Folder.save({ title: 'folder1' });
				const n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: folder1.id });
				const n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: folder1.id });


				const folder2 = await Folder.save({ title: 'folder2' });
				const n3 = await Note.save({ title: 'task3', body: 'baz', parent_id: folder2.id });
				const n4 = await Note.save({ title: 'task4', body: 'blah', parent_id: folder2.id });


				await engine.syncTables();

				let rows = await engine.search('-notebook:folder1', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n3.id);
				expect(ids(rows)).toContain(n4.id);


				rows = await engine.search('-notebook:folder2', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

			}));

			it('should support both inclusion and exclusion of notebooks together', (async () => {

				const parentFolder = await Folder.save({ title: 'parent' });
				const n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: parentFolder.id });
				const n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: parentFolder.id });


				const subFolder = await Folder.save({ title: 'child', parent_id: parentFolder.id });
				await Note.save({ title: 'task3', body: 'baz', parent_id: subFolder.id });
				await Note.save({ title: 'task4', body: 'blah', parent_id: subFolder.id });


				await engine.syncTables();

				const rows = await engine.search('notebook:parent -notebook:child', { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(n1.id);
				expect(ids(rows)).toContain(n2.id);

			}));

			it('should support filtering by note id', (async () => {
				let rows;
				const note1 = await Note.save({ title: 'Note 1', body: 'body' });
				const note2 = await Note.save({ title: 'Note 2', body: 'body' });
				const note3 = await Note.save({ title: 'Note 3', body: 'body' });
				await engine.syncTables();

				rows = await engine.search(`id:${note1.id}`, { searchType });
				expect(rows.length).toBe(1);
				expect(ids(rows)).toContain(note1.id);

				rows = await engine.search(`any:1 id:${note1.id} id:${note2.id}`, { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(note1.id);
				expect(ids(rows)).toContain(note2.id);

				rows = await engine.search(`any:0 id:${note1.id} id:${note2.id}`, { searchType });
				expect(rows.length).toBe(0);

				rows = await engine.search(`-id:${note2.id}`, { searchType });
				expect(rows.length).toBe(2);
				expect(ids(rows)).toContain(note1.id);
				expect(ids(rows)).toContain(note3.id);
			}));

		});
	}

});
