import { promises as fs } from 'fs';
import { stripIndent } from 'common-tags';
import { SinonStub, stub } from 'sinon';
import { expect } from 'chai';

import * as fsUtils from '../src/lib/fs-utils';
import { Extlinux } from '../src/config/backends/extlinux';

describe('Extlinux Configuration', () => {
	const backend = new Extlinux();

	it('should parse a extlinux.conf file', () => {
		const text = stripIndent`\
			DEFAULT primary
			# CommentExtlinux files

			TIMEOUT 30
			MENU TITLE Boot Options
			LABEL primary
						MENU LABEL primary Image
						LINUX /Image
						FDT /boot/mycustomdtb.dtb
						APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
		`;

		// @ts-ignore accessing private method
		const parsed = Extlinux.parseExtlinuxFile(text);
		expect(parsed.globals).to.have.property('DEFAULT').that.equals('primary');
		expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30');
		expect(parsed.globals)
			.to.have.property('MENU TITLE')
			.that.equals('Boot Options');

		expect(parsed.labels).to.have.property('primary');
		const { primary } = parsed.labels;
		expect(primary).to.have.property('MENU LABEL').that.equals('primary Image');
		expect(primary).to.have.property('LINUX').that.equals('/Image');
		expect(primary)
			.to.have.property('FDT')
			.that.equals('/boot/mycustomdtb.dtb');
		expect(primary)
			.to.have.property('APPEND')
			.that.equals('${cbootargs} ${resin_kernel_root} ro rootwait');
	});

	it('should parse multiple service entries', () => {
		const text = stripIndent`\
			DEFAULT primary
			# Comment

			TIMEOUT 30
			MENU TITLE Boot Options
			LABEL primary
						LINUX test1
						FDT /boot/mycustomdtb.dtb
						APPEND test2
			LABEL secondary
						LINUX test3
						FDT /boot/mycustomdtb.dtb
						APPEND test4\
		`;

		// @ts-ignore accessing private method
		const parsed = Extlinux.parseExtlinuxFile(text);
		expect(parsed.labels).to.have.property('primary').that.deep.equals({
			LINUX: 'test1',
			FDT: '/boot/mycustomdtb.dtb',
			APPEND: 'test2',
		});
		expect(parsed.labels).to.have.property('secondary').that.deep.equals({
			LINUX: 'test3',
			FDT: '/boot/mycustomdtb.dtb',
			APPEND: 'test4',
		});
	});

	it('should parse configuration options from an extlinux.conf file', async () => {
		let text = stripIndent`\
			DEFAULT primary
			# Comment

			TIMEOUT 30
			MENU TITLE Boot Options
			LABEL primary
						MENU LABEL primary Image
						LINUX /Image
						FDT /boot/mycustomdtb.dtb
						APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3\
		`;

		let readFileStub = stub(fs, 'readFile').resolves(text);
		let parsed = backend.getBootConfig();

		await expect(parsed)
			.to.eventually.have.property('isolcpus')
			.that.equals('3');
		await expect(parsed)
			.to.eventually.have.property('fdt')
			.that.equals('/boot/mycustomdtb.dtb');
		readFileStub.restore();

		text = stripIndent`\
			DEFAULT primary
			# Comment

			TIMEOUT 30
			MENU TITLE Boot Options
			LABEL primary
						MENU LABEL primary Image
						LINUX /Image
						FDT /boot/mycustomdtb.dtb
						APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3,4,5\
		`;
		readFileStub = stub(fs, 'readFile').resolves(text);

		parsed = backend.getBootConfig();

		readFileStub.restore();

		await expect(parsed)
			.to.eventually.have.property('isolcpus')
			.that.equals('3,4,5');
	});

	it('only matches supported devices', async () => {
		for (const { deviceType, metaRelease, supported } of MATCH_TESTS) {
			await expect(
				backend.matches(deviceType, metaRelease),
			).to.eventually.equal(supported);
		}
	});

	it('errors when cannot find extlinux.conf', async () => {
		// Stub readFile to reject much like if the file didn't exist
		stub(fs, 'readFile').rejects();
		await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
			'Could not find extlinux file. Device is possibly bricked',
		);
		// Restore stub
		(fs.readFile as SinonStub).restore();
	});

	it('throws error for malformed extlinux.conf', async () => {
		for (const badConfig of MALFORMED_CONFIGS) {
			// Stub bad config
			stub(fs, 'readFile').resolves(badConfig.contents);
			// Expect correct rejection from the given bad config
			try {
				await backend.getBootConfig();
			} catch (e) {
				expect(e.message).to.equal(badConfig.reason);
			}
			// Restore stub
			(fs.readFile as SinonStub).restore();
		}
	});

	it('parses supported config values from bootConfigPath', async () => {
		// Will try to parse /test/data/mnt/boot/extlinux/extlinux.conf
		await expect(backend.getBootConfig()).to.eventually.deep.equal({}); // None of the values are supported so returns empty

		// Stub readFile to return a config that has supported values
		stub(fs, 'readFile').resolves(stripIndent`
    DEFAULT primary
    TIMEOUT 30
		MENU TITLE Boot Options
    LABEL primary
					MENU LABEL primary Image
					LINUX /Image
					FDT /boot/mycustomdtb.dtb
          APPEND ro rootwait isolcpus=0,4
    `);

		await expect(backend.getBootConfig()).to.eventually.deep.equal({
			isolcpus: '0,4',
			fdt: '/boot/mycustomdtb.dtb',
		});

		// Restore stub
		(fs.readFile as SinonStub).restore();
	});

	it('sets new config values', async () => {
		stub(fsUtils, 'writeFileAtomic').resolves();
		stub(fsUtils, 'exec').resolves();

		await backend.setBootConfig({
			fdt: '/boot/mycustomdtb.dtb',
			isolcpus: '2',
		});

		expect(fsUtils.writeFileAtomic).to.be.calledWith(
			'./test/data/mnt/boot/extlinux/extlinux.conf',
			stripIndent`
	      DEFAULT primary
	      TIMEOUT 30
	      MENU TITLE Boot Options
	      LABEL primary
							MENU LABEL primary Image
							LINUX /Image
							APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2
							FDT /boot/mycustomdtb.dtb
	    ` + '\n', // add newline because stripIndent trims last newline
		);

		// Restore stubs
		(fsUtils.writeFileAtomic as SinonStub).restore();
		(fsUtils.exec as SinonStub).restore();
	});

	it('only allows supported configuration options', () => {
		[
			{ configName: 'isolcpus', supported: true },
			{ configName: 'fdt', supported: true },
			{ configName: '', supported: false },
			{ configName: 'ro', supported: false }, // not allowed to configure
			{ configName: 'rootwait', supported: false }, // not allowed to configure
		].forEach(({ configName, supported }) =>
			expect(backend.isSupportedConfig(configName)).to.equal(supported),
		);
	});

	it('correctly detects boot config variables', () => {
		[
			{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
			{ config: 'HOST_EXTLINUX_fdt', valid: true },
			{ config: 'HOST_EXTLINUX_rootwait', valid: true },
			{ config: 'HOST_EXTLINUX_5', valid: true },
			// TO-DO: { config: 'HOST_EXTLINUX', valid: false },
			// TO-DO: { config: 'HOST_EXTLINUX_', valid: false },
			{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
			{ config: 'isolcpus', valid: false },
		].forEach(({ config, valid }) =>
			expect(backend.isBootConfigVar(config)).to.equal(valid),
		);
	});

	it('converts variable to backend formatted name', () => {
		[
			{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
			{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
			{ input: 'HOST_EXTLINUX_rootwait', output: 'rootwait' },
			{ input: 'HOST_EXTLINUX_something_else', output: 'something_else' },
			{ input: 'HOST_EXTLINUX_', output: 'HOST_EXTLINUX_' },
			{ input: 'HOST_EXTLINUX_ ', output: ' ' },
			{ input: 'ROOT_EXTLINUX_isolcpus', output: 'ROOT_EXTLINUX_isolcpus' },
		].forEach(({ input, output }) =>
			expect(backend.processConfigVarName(input)).to.equal(output),
		);
	});

	it('normalizes variable value', () => {
		[
			{ input: { key: 'key', value: 'value' }, output: 'value' },
		].forEach(({ input, output }) =>
			expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
				output,
			),
		);
	});

	it('returns the environment name for config variable', () => {
		[
			{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
			{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
			{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
			{ input: '', output: 'HOST_EXTLINUX_' },
			{ input: '5', output: 'HOST_EXTLINUX_5' },
		].forEach(({ input, output }) =>
			expect(backend.createConfigVarName(input)).to.equal(output),
		);
	});
});

const MALFORMED_CONFIGS = [
	{
		contents: stripIndent`
    TIMEOUT 30
    MENU TITLE Boot Options
    LABEL primary
          MENU LABEL primary Image
          LINUX /Image
          APPEND ro rootwait isolcpus=0,4
    `,
		reason: 'Could not find default entry for extlinux.conf file',
	},
	{
		contents: stripIndent`
    DEFAULT typo_oops
    TIMEOUT 30
    MENU TITLE Boot Options
    LABEL primary
          MENU LABEL primary Image
          LINUX /Image
          APPEND ro rootwait isolcpus=0,4
    `,
		reason: 'Cannot find label entry (label: typo_oops) for extlinux.conf file',
	},
	{
		contents: stripIndent`
    DEFAULT primary
    TIMEOUT 30
    MENU TITLE Boot Options
    LABEL primary
          MENU LABEL primary Image
          LINUX /Image
    `,
		reason:
			'Could not find APPEND directive in default extlinux.conf boot entry',
	},
	{
		contents: stripIndent`
    DEFAULT primary
    TIMEOUT 30
    MENU TITLE Boot Options
    LABEL primary
          MENU LABEL primary Image
          LINUX /Image
          APPEND ro rootwait isolcpus=0,4=woops
    `,
		reason: 'Unable to parse invalid value: isolcpus=0,4=woops',
	},
];

const SUPPORTED_VERSION = '2.45.0'; // or less
const UNSUPPORTED_VERSION = '2.47.0'; // or greater

const MATCH_TESTS = [
	{
		deviceType: 'jetson-tx1',
		metaRelease: SUPPORTED_VERSION,
		supported: true,
	},
	{
		deviceType: 'jetson-tx2',
		metaRelease: SUPPORTED_VERSION,
		supported: true,
	},
	{
		deviceType: 'jetson-tx2',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'jetson-nano',
		metaRelease: SUPPORTED_VERSION,
		supported: true,
	},
	{
		deviceType: 'jetson-nano',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'jetson-xavier',
		metaRelease: SUPPORTED_VERSION,
		supported: true,
	},
	{
		deviceType: 'jetson-xavier',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'intel-nuc',
		metaRelease: SUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'intel-nuc',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'raspberry',
		metaRelease: SUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'raspberry',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'fincm3',
		metaRelease: SUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'fincm3',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'up-board',
		metaRelease: SUPPORTED_VERSION,
		supported: false,
	},
	{
		deviceType: 'up-board',
		metaRelease: UNSUPPORTED_VERSION,
		supported: false,
	},
];
