import { expectNotThrow, naughtyStrings, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Note from '../models/Note';
import Revision, { ObjectPatch } from '../models/Revision';

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

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

	it('should create patches of text and apply it', (async () => {
		const note1 = await Note.save({ body: 'my note\nsecond line' });

		const patch = Revision.createTextPatch(note1.body, 'my new note\nsecond line');
		const merged = Revision.applyTextPatch(note1.body, patch);

		expect(merged).toBe('my new note\nsecond line');
	}));

	it('should check if it is an empty revision', async () => {
		const testCases = [
			[false, {
				title_diff: '',
				body_diff: '',
				metadata_diff: '{"new":{"id":"aaa"},"deleted":[]}',
			}],
			[true, {
				title_diff: '',
				body_diff: '',
				metadata_diff: '',
			}],
			[true, {
				title_diff: '[]',
				body_diff: '',
				metadata_diff: '{"new":{},"deleted":[]}',
			}],
			[true, {
				title_diff: '',
				body_diff: '[]',
				metadata_diff: '{"new":{},"deleted":[]}',
			}],
			[false, {
				title_diff: '[{"diffs":[[1,"hello"]],"start1":0,"start2":0,"length1":0,"length2":5}]',
				body_diff: '[]',
				metadata_diff: '{"new":{},"deleted":[]}',
			}],
		];

		for (const t of testCases) {
			const [expected, input] = t;
			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
			expect(Revision.isEmptyRevision(input as any)).toBe(expected);
		}
	});

	it('should not fail to create revisions on naughty strings', (async () => {
		// Previously this pattern would fail:
		// - Create a patch between an empty string and smileys
		// - Use that patch on the empty string to get back the smileys
		// - Create a patch between those smileys and new smileys
		// https://github.com/JackuB/diff-match-patch/issues/22

		const nss = await naughtyStrings();

		// First confirm that it indeed fails with the legacy approach.
		let errorCount = 0;

		for (let i = 0; i < nss.length - 1; i++) {
			const ns1 = nss[i];
			const ns2 = nss[i + 1];
			try {
				const patchText = Revision.createTextPatchLegacy('', ns1);
				const patchedText = Revision.applyTextPatchLegacy('', patchText);
				Revision.createTextPatchLegacy(patchedText, ns2);
			} catch (error) {
				errorCount++;
			}
		}

		expect(errorCount).toBe(10);

		// Now feed the naughty list again but using the new approach. In that
		// case it should work fine.
		await expectNotThrow(async () => {
			for (let i = 0; i < nss.length - 1; i++) {
				const ns1 = nss[i];
				const ns2 = nss[i + 1];
				const patchText = Revision.createTextPatch('', ns1);
				const patchedText = Revision.applyTextPatch('', patchText);
				Revision.createTextPatch(patchedText, ns2);
			}
		});
	}));

	it('should successfully handle legacy patches', async () => {
		// The code should handle applying a series of new style patches and
		// legacy patches, and the correct text should be recovered at the end.
		const changes = [
			'',
			'one',
			'one three',
			'one two three',
		];

		const patches = [
			Revision.createTextPatch(changes[0], changes[1]),
			Revision.createTextPatchLegacy(changes[1], changes[2]),
			Revision.createTextPatch(changes[2], changes[3]),
		];

		// Sanity check - verify that the patches are as expected
		expect(patches[0].substr(0, 2)).toBe('[{'); // New
		expect(patches[1].substr(0, 2)).toBe('@@'); // Legacy
		expect(patches[2].substr(0, 2)).toBe('[{'); // New

		let finalString = Revision.applyTextPatch(changes[0], patches[0]);
		finalString = Revision.applyTextPatch(finalString, patches[1]);
		finalString = Revision.applyTextPatch(finalString, patches[2]);

		expect(finalString).toBe('one two three');
	});

	it('should create patches of objects and apply it', (async () => {
		const oldObject = {
			one: '123',
			two: '456',
			three: '789',
		};

		const newObject = {
			one: '123',
			three: '999',
		};

		const patch = Revision.createObjectPatch(oldObject, newObject);
		const merged = Revision.applyObjectPatch(oldObject, patch);

		expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
	}));

	it('should handle invalid object patch', (async () => {
		const oldObject = {
			one: '123',
			two: '456',
			three: '789',
		};

		const brokenPatch = `{"new":{"four":"444
"},"deleted":["one"]}`;

		const expected = {
			two: '456',
			three: '789',
			four: '444',
		};

		const merged = Revision.applyObjectPatch(oldObject, brokenPatch);

		expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
	}));

	it('should not strip off newlines from object values', (async () => {
		const oldObject = {
			one: '123',
			two: '456',
			three: '789',
		};

		const patch: ObjectPatch = {
			'new': {
				'four': 'one line\ntwo line',
			},
			'deleted': [],
		};

		const expected = {
			one: '123',
			two: '456',
			three: '789',
			four: 'one line\ntwo line',
		};

		const merged = Revision.applyObjectPatch(oldObject, JSON.stringify(patch));

		expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
	}));

	it('should move target revision to the top', (async () => {
		const revs = [
			{ id: '123' },
			{ id: '456' },
			{ id: '789' },
		];

		let newRevs;
		newRevs = Revision.moveRevisionToTop({ id: '456' }, revs);
		expect(newRevs[0].id).toBe('123');
		expect(newRevs[1].id).toBe('789');
		expect(newRevs[2].id).toBe('456');

		newRevs = Revision.moveRevisionToTop({ id: '789' }, revs);
		expect(newRevs[0].id).toBe('123');
		expect(newRevs[1].id).toBe('456');
		expect(newRevs[2].id).toBe('789');
	}));

	// cSpell:disable
	it('should create patch stats', (async () => {
		const tests = [
			{
				patch: `@@ -625,16 +625,48 @@
 rrupted download
+%0A- %5B %5D Fix mobile screen options`,
				expected: [-0, +32],
			},
			{
				patch: `@@ -564,17 +564,17 @@
 ages%0A- %5B
- 
+x
 %5D Check `,
				expected: [-1, +1],
			},
			{
				patch: `@@ -1022,56 +1022,415 @@
 .%0A%0A#
- How to view a note history%0A%0AWhile all the apps 
+%C2%A0How does it work?%0A%0AAll the apps save a version of the modified notes every 10 minutes.
 %0A%0A# `,
				expected: [-(19 + 27 + 2), 17 + 67 + 4],
			},
			{
				patch: '',
				expected: [-0, +0],
			},
		];

		for (const test of tests) {
			const stats = Revision.patchStats(test.patch);
			expect(stats.removed).toBe(-test.expected[0]);
			expect(stats.added).toBe(test.expected[1]);
		}
	}));
	// cSpell:enable

});
