import { setupDatabaseAndSynchronizer, switchClient, checkThrowAsync } from '../testing/test-utils';
import Folder from '../models/Folder';
import Note from '../models/Note';
import Tag from '../models/Tag';

describe('models/Tag', () => {

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

	it('should add tags by title', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });

		await Tag.setNoteTagsByTitles(note1.id, ['un', 'deux']);

		const noteTags = await Tag.tagsByNoteId(note1.id);
		expect(noteTags.length).toBe(2);
	});

	it('should get the notes associated with a tag', async () => {
		const note1 = await Note.save({});
		const note2 = await Note.save({});
		const note3 = await Note.save({});

		await Tag.setNoteTagsByTitles(note1.id, ['un']);
		await Tag.setNoteTagsByTitles(note2.id, ['un']);
		await Tag.setNoteTagsByTitles(note3.id, ['deux']);

		await Tag.save({ title: 'trois' });

		const tag1 = await Tag.loadByTitle('un');
		const tag2 = await Tag.loadByTitle('deux');
		const tag3 = await Tag.loadByTitle('trois');

		expect((await Tag.noteIds(tag1.id)).sort()).toEqual([note1.id, note2.id].sort());
		expect((await Tag.noteIds(tag2.id)).sort()).toEqual([note3.id].sort());
		expect((await Tag.noteIds(tag3.id)).sort()).toEqual([].sort());

		expect(await Tag.hasNote(tag1.id, note1.id)).toBe(true);
		expect(await Tag.hasNote(tag1.id, note2.id)).toBe(true);
		expect(await Tag.hasNote(tag1.id, note3.id)).toBe(false);
		expect(await Tag.hasNote(tag2.id, note1.id)).toBe(false);
		expect(await Tag.hasNote(tag2.id, note2.id)).toBe(false);
		expect(await Tag.hasNote(tag2.id, note3.id)).toBe(true);
		expect(await Tag.hasNote(tag3.id, note1.id)).toBe(false);
		expect(await Tag.hasNote(tag3.id, note2.id)).toBe(false);
		expect(await Tag.hasNote(tag3.id, note3.id)).toBe(false);

		const notesTag1 = await Tag.notes(tag1.id);
		const notesTag2 = await Tag.notes(tag2.id);
		const notesTag3 = await Tag.notes(tag3.id);
		expect(notesTag1.map(n => n.id).sort()).toEqual([note1.id, note2.id].sort());
		expect(notesTag2.map(n => n.id).sort()).toEqual([note3.id].sort());
		expect(notesTag3.map(n => n.id).sort()).toEqual([].sort());
	});

	it('should not retrieve deleted notes', async () => {
		const note1 = await Note.save({});
		const note2 = await Note.save({});

		await Tag.setNoteTagsByTitles(note1.id, ['un']);
		await Tag.setNoteTagsByTitles(note2.id, ['un']);

		const tag1 = await Tag.loadByTitle('un');

		await Note.delete(note1.id, { toTrash: true });

		expect(await Tag.noteIds(tag1.id)).toEqual([note2.id]);
		expect((await Tag.notes(tag1.id)).map(n => n.id).sort()).toEqual([note2.id]);
		expect(await Tag.hasNote(tag1.id, note1.id)).toBe(false);
		expect(await Tag.hasNote(tag1.id, note2.id)).toBe(true);

		const allWithNotes = await Tag.allWithNotes();
		expect(allWithNotes[0].note_count).toBe(1);
	});

	it('should not allow renaming tag to existing tag names', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });

		await Tag.setNoteTagsByTitles(note1.id, ['un', 'deux']);

		const tagUn = await Tag.loadByTitle('un');
		const hasThrown = await checkThrowAsync(async () => await Tag.save({ id: tagUn.id, title: 'deux' }, { userSideValidation: true }));

		expect(hasThrown).toBe(true);
	});

	it('should not return tags without notes', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
		await Tag.setNoteTagsByTitles(note1.id, ['un']);

		let tags = await Tag.allWithNotes();
		expect(tags.length).toBe(1);

		await Note.delete(note1.id);

		tags = await Tag.allWithNotes();
		expect(tags.length).toBe(0);
	});

	it('should return tags with note counts', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
		const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
		const todo1 = await Note.save({ title: 'todo 1', parent_id: folder1.id, is_todo: 1, todo_completed: 1590085027710 });
		await Tag.setNoteTagsByTitles(note1.id, ['un']);
		await Tag.setNoteTagsByTitles(note2.id, ['un']);
		await Tag.setNoteTagsByTitles(todo1.id, ['un']);

		let tags = await Tag.allWithNotes();
		expect(tags.length).toBe(1);
		expect(tags[0].note_count).toBe(3);
		expect(tags[0].todo_completed_count).toBe(1);

		await Note.delete(todo1.id);

		tags = await Tag.allWithNotes();
		expect(tags.length).toBe(1);
		expect(tags[0].note_count).toBe(2);

		await Note.delete(note1.id);

		tags = await Tag.allWithNotes();
		expect(tags.length).toBe(1);
		expect(tags[0].note_count).toBe(1);

		await Note.delete(note2.id);

		tags = await Tag.allWithNotes();
		expect(tags.length).toBe(0);
	});

	it('should load individual tags with note count', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
		const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
		const todo1 = await Note.save({ title: 'todo 2', parent_id: folder1.id, is_todo: 1, todo_completed: 1590085027710 });
		const todo2 = await Note.save({ title: 'todo 2', parent_id: folder1.id, is_todo: 1 });
		const tag = await Tag.save({ title: 'mytag' });
		await Tag.addNote(tag.id, note1.id);

		let tagWithCount = await Tag.loadWithCount(tag.id);
		expect(tagWithCount.note_count).toBe(1);

		await Tag.addNote(tag.id, note2.id);
		tagWithCount = await Tag.loadWithCount(tag.id);
		expect(tagWithCount.note_count).toBe(2);

		await Tag.addNote(tag.id, todo1.id);
		await Tag.addNote(tag.id, todo2.id);
		tagWithCount = await Tag.loadWithCount(tag.id);
		expect(tagWithCount.note_count).toBe(4);
		expect(tagWithCount.todo_completed_count).toBe(1);
	});

	it('should get common tags for set of notes', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const taga = await Tag.save({ title: 'mytaga' });
		const tagb = await Tag.save({ title: 'mytagb' });
		const tagc = await Tag.save({ title: 'mytagc' });
		await Tag.save({ title: 'mytagd' });

		const note0 = await Note.save({ title: 'ma note 0', parent_id: folder1.id });
		const note1 = await Note.save({ title: 'ma note 1', parent_id: folder1.id });
		const note2 = await Note.save({ title: 'ma note 2', parent_id: folder1.id });
		const note3 = await Note.save({ title: 'ma note 3', parent_id: folder1.id });

		await Tag.addNote(taga.id, note1.id);

		await Tag.addNote(taga.id, note2.id);
		await Tag.addNote(tagb.id, note2.id);

		await Tag.addNote(taga.id, note3.id);
		await Tag.addNote(tagb.id, note3.id);
		await Tag.addNote(tagc.id, note3.id);

		let commonTags = await Tag.commonTagsByNoteIds(null);
		expect(commonTags.length).toBe(0);

		commonTags = await Tag.commonTagsByNoteIds(undefined);
		expect(commonTags.length).toBe(0);

		commonTags = await Tag.commonTagsByNoteIds([]);
		expect(commonTags.length).toBe(0);

		commonTags = await Tag.commonTagsByNoteIds([note0.id, note1.id, note2.id, note3.id]);
		let commonTagIds = commonTags.map(t => t.id);
		expect(commonTagIds.length).toBe(0);

		commonTags = await Tag.commonTagsByNoteIds([note1.id, note2.id, note3.id]);
		commonTagIds = commonTags.map(t => t.id);
		expect(commonTagIds.length).toBe(1);
		expect(commonTagIds.includes(taga.id)).toBe(true);

		commonTags = await Tag.commonTagsByNoteIds([note2.id, note3.id]);
		commonTagIds = commonTags.map(t => t.id);
		expect(commonTagIds.length).toBe(2);
		expect(commonTagIds.includes(taga.id)).toBe(true);
		expect(commonTagIds.includes(tagb.id)).toBe(true);

		commonTags = await Tag.commonTagsByNoteIds([note3.id]);

		commonTagIds = commonTags.map(t => t.id);
		expect(commonTags.length).toBe(3);
		expect(commonTagIds.includes(taga.id)).toBe(true);
		expect(commonTagIds.includes(tagb.id)).toBe(true);
		expect(commonTagIds.includes(tagc.id)).toBe(true);
	});

	it('should allow finding tags when case does not match, for standard ASCII characters', async () => {
		const note1 = await Note.save({});
		await Tag.setNoteTagsByTitles(note1.id, ['Hello']);
		const tag1 = await Tag.loadByTitle('Hello');
		const tag2 = await Tag.loadByTitle('hello');
		expect(tag1).toStrictEqual(tag2);
	});

	it('should not create duplicate tags when tagging with existing titles (issue #14540)', async () => {
		const folder1 = await Folder.save({ title: 'folder1' });
		const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });

		// Test 1: Simple existing tag
		const tag1 = await Tag.save({ title: 'meetings' });
		await Tag.setNoteTagsByTitles(note1.id, ['meetings']);
		let allTags = await Tag.all();
		expect(allTags.length).toBe(1);
		expect(allTags[0].id).toBe(tag1.id);

		// Test 2: Whitespace issues
		await Tag.setNoteTagsByTitles(note1.id, ['meetings ']);
		allTags = await Tag.all();
		expect(allTags.length).toBe(1);

		// Test 3: Normalization issues
		const composed = '\u00E9';
		const decomposed = '\u0065\u0301';
		await Tag.save({ title: composed });
		await Tag.setNoteTagsByTitles(note1.id, [decomposed]);
		allTags = await Tag.all();
		// In addition to 'meetings', we should have only one 'é' tag
		expect(allTags.length).toBe(2);

		// Test 4: Case change should not create new tag but match existing
		await Tag.setNoteTagsByTitles(note1.id, ['MEETINGS']);
		allTags = await Tag.all();
		expect(allTags.length).toBe(2);
	});

	it('should prefer an exact legacy tag match before the normalized fallback', async () => {
		const decomposed = '\u0065\u0301';
		const composed = '\u00E9';
		const legacyTagId = Tag.generateUuid();
		const normalizedTag = await Tag.save({ title: composed });

		await Tag.db().exec('INSERT INTO tags (id, title, created_time, updated_time) VALUES (?, ?, ?, ?)', [legacyTagId, decomposed, 1, 1]);

		const loadedTag = await Tag.loadByTitle(decomposed);
		expect(loadedTag.id).toBe(legacyTagId);
		expect(loadedTag.id).not.toBe(normalizedTag.id);
	});
});
