import { afterAllTests, beforeAllDb, beforeEachDb, createFolder, createUserAndSession, db, dbSlave, expectThrow, getDatabaseClientType, models, packageRootDir, updateFolder } from './utils/testing/testUtils';
import { connectDb, disconnectDb, reconnectDb, sqliteSyncSlave } from './db';
import { ChangeType, Event } from './services/database/types';
import { DatabaseConfig, DatabaseConfigClient } from './utils/types';
import { createDb } from './tools/dbTools';
import { msleep } from './utils/time';
import { FolderEntity } from '@joplin/lib/services/database/types';

const eventId1 = '4f405391-bd72-4a4f-809f-344fc6cd4b31';
const eventId2 = '4f405391-bd72-4a4f-809f-344fc6cd4b32';

const event1: Event = {
	id: eventId1,
	type: 1,
	name: 'test',
	created_time: Date.now(),
};

const event2 = {
	...event1,
	id: eventId2,
};

let testIndex = 0;
const beforeTest = async (envValues: Record<string, string> = null) => {
	// Use `beforeAllDb` in `beforeEach` to ensure each test has its own database.
	// To work around file locking issues on Windows, each test needs its own database instance:
	const databaseKey = `db.replication.${testIndex ++}`;
	await beforeAllDb(databaseKey, envValues ? { envValues } : null);
	await beforeEachDb();
};

const afterTest = async () => {
	await afterAllTests();
};

describe('db.replication', () => {

	afterEach(async () => {
		await afterTest();
	});

	it('should reconnect a database', async () => {
		if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;

		await beforeTest();

		await disconnectDb(db());
		await expectThrow(async () => db().insert(event1).into('events'));

		await reconnectDb(db());
		await db().insert(event1).into('events');

		{
			const results = await db().select('*').from('events');
			expect(results.length).toBe(1);
			expect(results[0].id).toBe(eventId1);
		}

		await reconnectDb(db());
		await db().insert(event2).into('events');

		{
			const results = await db().select('*').from('events');
			expect(results.length).toBe(2);
			expect([results[0].id, results[1].id].sort()).toEqual([eventId1, eventId2]);
		}
	});

	it('should manually sync an SQLite slave instance', async () => {
		if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;

		await beforeTest();

		const masterConfig: DatabaseConfig = {
			client: DatabaseConfigClient.SQLite,
			name: `${packageRootDir}/db-master-test.sqlite`,
		};

		const slaveConfig: DatabaseConfig = {
			client: DatabaseConfigClient.SQLite,
			name: `${packageRootDir}/db-slave-test.sqlite`,
		};

		await createDb(masterConfig, { dropIfExists: true });
		await createDb(slaveConfig, { dropIfExists: true });

		const master = await connectDb(masterConfig);
		const slave = await connectDb(slaveConfig);

		await master.insert(event1).into('events');

		expect((await master.select('*').from('events')).length).toBe(1);
		expect((await slave.select('*').from('events')).length).toBe(0);

		await sqliteSyncSlave(master, slave);

		expect((await master.select('*').from('events')).length).toBe(1);
		expect((await slave.select('*').from('events')).length).toBe(1);

		await disconnectDb(master);
		await disconnectDb(slave);
	});

	test('should track changes - using replication', async () => {
		if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;

		await beforeTest({ DB_USE_SLAVE: '1' });

		const { session, user } = await createUserAndSession(1, true);
		const changeModel = models().change();

		const folder = {
			id: '000000000000000000000000000000F1',
			title: 'title 1',
		};

		const folderItem = await createFolder(session.id, folder);
		await msleep(1);

		let result = await changeModel.delta(user.id);

		// We get nothing because the slave has not been synced yet
		expect(result.items.length).toBe(0);

		// But we still get the item because it doesn't use the slave database
		expect((await models().item().loadAsJoplinItem<FolderEntity>(folderItem.id)).title).toBe('title 1');

		// After sync, we should get the change
		await sqliteSyncSlave(db(), dbSlave());
		result = await changeModel.delta(user.id);
		expect(result.items.length).toBe(1);
		expect(result.items[0].type).toBe(ChangeType.Create);

		await updateFolder(session.id, { ...folder, title: 'title 2' });
		result = await changeModel.delta(user.id, { cursor: result.cursor });

		// Nothing because it hasn't been synced yet
		expect(result.items.length).toBe(0);

		// But we get the latest item if requesting it directly
		expect((await models().item().loadAsJoplinItem<FolderEntity>(folderItem.id)).title).toBe('title 2');

		// After sync, we should get the change
		await sqliteSyncSlave(db(), dbSlave());
		result = await changeModel.delta(user.id, { cursor: result.cursor });

		expect(result.items.length).toBe(1);
		expect(result.items[0].type).toBe(ChangeType.Update);
	});

});
