import type { Knex } from 'knex';
import { knex } from 'knex';
import { promises as fs } from 'fs';

import { expect } from 'chai';
import * as constants from '~/lib/constants';

async function createOldDatabase(path: string) {
	const db = knex({
		client: 'sqlite3',
		connection: {
			filename: path,
		},
		useNullAsDefault: true,
	});

	const createEmptyTable = (
		name: string,
		fn: (trx: Knex.CreateTableBuilder) => void,
	) =>
		db.schema.createTable(name, (t) => {
			if (fn != null) {
				return fn(t);
			}
		});

	await createEmptyTable('app', (t) => {
		t.increments('id').primary();
		t.boolean('privileged');
		return t.string('containerId');
	});
	await createEmptyTable('config', (t) => {
		t.string('key');
		return t.string('value');
	});
	await createEmptyTable('dependentApp', (t) => t.increments('id').primary());
	await createEmptyTable('dependentDevice', (t) =>
		t.increments('id').primary(),
	);
	return db;
}

async function restoreDb() {
	await fs.unlink(constants.databasePath).catch(() => {
		/* NOOP */
	});
	// Reset the module cache to allow the database to be initialized again
	delete require.cache[require.resolve('~/src/db')];
}

// Utility method to use along with `require`
type Db = typeof import('~/src/db');

describe('db', () => {
	afterEach(async () => {
		await restoreDb();
	});

	it('creates a database at the path passed on creation', async () => {
		// eslint-disable-next-line
		const testDb = require('~/src/db') as Db;
		await testDb.initialized();
		await expect(fs.access(constants.databasePath)).to.not.be.rejected;
	});

	it('migrations add new fields and removes old ones in an old database', async () => {
		// Create a database with an older schema
		const knexForDB = await createOldDatabase(constants.databasePath);
		// eslint-disable-next-line
		const testDb = require('~/src/db') as Db;
		await testDb.initialized();
		await Promise.all([
			expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true,
			expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be
				.true,
			expect(knexForDB.schema.hasColumn('app', 'config')).to.eventually.be
				.false,
			expect(knexForDB.schema.hasColumn('app', 'privileged')).to.eventually.be
				.false,
			expect(knexForDB.schema.hasColumn('app', 'containerId')).to.eventually.be
				.false,
			expect(knexForDB.schema.hasTable('dependentDeviceTarget')).to.eventually
				.be.false,
			expect(knexForDB.schema.hasTable('dependentDevice')).to.eventually.be
				.false,
			expect(knexForDB.schema.hasTable('dependentAppTarget')).to.eventually.be
				.false,
			expect(knexForDB.schema.hasTable('dependentApp')).to.eventually.be.false,
		]);
	});

	it('creates a deviceConfig table with a single default value', async () => {
		// eslint-disable-next-line
		const testDb = require('~/src/db') as Db;
		await testDb.initialized();
		const deviceConfig = await testDb.models('deviceConfig').select();
		expect(deviceConfig).to.have.lengthOf(1);
		expect(deviceConfig).to.deep.equal([{ targetValues: '{}' }]);
	});

	it('allows performing transactions', async () => {
		// eslint-disable-next-line
		const testDb = require('~/src/db') as Db;
		await testDb.initialized();
		return testDb.transaction((trx) => expect(trx.commit()).to.be.fulfilled);
	});
});
