import { stripIndent } from 'common-tags';
import { expect } from 'chai';
import { testfs } from 'mocha-pod';
import { promises as fs } from 'fs';

import * as hostUtils from '~/lib/host-utils';
import log from '~/lib/supervisor-console';
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';

describe('config/extra-uEnv', () => {
	const backend = new ExtraUEnv();

	it('should parse extra_uEnv string', () => {
		const fileContents = stripIndent`\
      custom_fdt_file=mycustom.dtb
      extra_os_cmdline=isolcpus=3,4 splash console=tty0
		`;
		// @ts-expect-error accessing private method
		const parsed = ExtraUEnv.parseOptions(fileContents);
		expect(parsed).to.deep.equal({
			fdt: 'mycustom.dtb',
			isolcpus: '3,4',
			splash: '',
			console: 'tty0',
		});
	});

	it('should only parse supported configuration options from bootConfigPath', async () => {
		let tfs = await testfs({
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	    	custom_fdt_file=mycustom.dtb
      	extra_os_cmdline=isolcpus=3,4
			`,
		}).enable();

		await expect(backend.getBootConfig()).to.eventually.deep.equal({
			fdt: 'mycustom.dtb',
			isolcpus: '3,4',
		});

		await tfs.restore();

		// Add other options that will get filtered out because they aren't supported
		tfs = await testfs({
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	    	custom_fdt_file=mycustom.dtb
        extra_os_cmdline=isolcpus=3,4 console=tty0 splash
			`,
		}).enable();

		await expect(backend.getBootConfig()).to.eventually.deep.equal({
			fdt: 'mycustom.dtb',
			isolcpus: '3,4',
		});

		await tfs.restore();

		// Configuration with no supported values
		tfs = await testfs({
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	    	fdt=something_else
			 	isolcpus
				123.12=5
			`,
		}).enable();
		await expect(backend.getBootConfig()).to.eventually.deep.equal({});

		await tfs.restore();
	});

	it('only matches supported devices', async () => {
		// The file exists before
		const tfs = await testfs({
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	    	custom_fdt_file=mycustom.dtb
      	extra_os_cmdline=isolcpus=3,4
			`,
		}).enable();
		for (const device of MATCH_TESTS) {
			// Test device that has extra_uEnv.txt
			await expect(backend.matches(device.type)).to.eventually.equal(
				device.supported,
			);
		}

		await tfs.restore();

		// The file no longer exists
		await expect(
			fs.access(hostUtils.pathOnBoot('extra_uEnv.txt')),
			'extra_uEnv.txt does not exist before the test',
		).to.be.rejected;
		for (const device of MATCH_TESTS) {
			// Test same device but without extra_uEnv.txt
			await expect(backend.matches(device.type)).to.eventually.be.false;
		}
	});

	it('errors when cannot find extra_uEnv.txt', async () => {
		// The file no longer exists
		await expect(
			fs.access(hostUtils.pathOnBoot('extra_uEnv.txt')),
			'extra_uEnv.txt does not exist before the test',
		).to.be.rejected;
		await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
			'Could not find extra_uEnv file. Device is possibly bricked',
		);
	});

	it('logs warning for malformed extra_uEnv.txt', async () => {
		for (const badConfig of MALFORMED_CONFIGS) {
			// Setup the environment with a bad config
			const tfs = await testfs({
				[hostUtils.pathOnBoot('extra_uEnv.txt')]: badConfig.contents,
			}).enable();

			// Expect warning log from the given bad config
			await backend.getBootConfig();

			expect(log.warn).to.have.been.calledWith(badConfig.reason);
			await tfs.restore();
		}
	});

	it('sets new config values', async () => {
		const tfs = await testfs({
			// This config contains a value set from something else
			// We to make sure the Supervisor is enforcing the source of truth (the cloud)
			// So after setting new values this unsupported/not set value should be gone
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	    	extra_os_cmdline=rootwait isolcpus=3,4
	     other_service=set_this_value
			`,
		}).enable();

		// Sets config with mix of supported and not supported values
		await backend.setBootConfig({
			fdt: '/boot/mycustomdtb.dtb',
			isolcpus: '2',
			console: 'tty0', // not supported so won't be set
		});

		// Confirm that the file was written correctly
		await expect(
			fs.readFile(hostUtils.pathOnBoot('extra_uEnv.txt'), 'utf8'),
		).to.eventually.equal(
			'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2\n',
		);

		expect(log.warn).to.have.been.calledWith(
			'Not setting unsupported value: { console: tty0 }',
		);

		await tfs.restore();
	});

	it('sets new config values containing collections', async () => {
		// @ts-expect-error accessing private value
		const previousSupportedConfigs = ExtraUEnv.supportedConfigs;
		// Stub isSupportedConfig so we can confirm collections work
		// @ts-expect-error accessing private value
		ExtraUEnv.supportedConfigs = {
			fdt: { key: 'custom_fdt_file', collection: false },
			isolcpus: { key: 'extra_os_cmdline', collection: true },
			console: { key: 'extra_os_cmdline', collection: true },
			splash: { key: 'extra_os_cmdline', collection: true },
		};

		const tfs = await testfs({
			[hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent`
	     	other_service=set_this_value
			`,
		}).enable();

		// Set config again
		await backend.setBootConfig({
			fdt: '/boot/mycustomdtb.dtb',
			isolcpus: '2', // collection entry so should be concatted to other collections of this entry
			console: 'tty0', // collection entry so should be concatted to other collections of this entry
			splash: '', // collection entry so should be concatted to other collections of this entry
		});

		await expect(
			fs.readFile(hostUtils.pathOnBoot('extra_uEnv.txt'), 'utf8'),
		).to.eventually.equal(
			'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2 console=tty0 splash\n',
		);

		// @ts-expect-error accessing private value
		ExtraUEnv.supportedConfigs = previousSupportedConfigs;

		await tfs.restore();
	});
});

const MALFORMED_CONFIGS = [
	{
		contents: stripIndent`
      custom_fdt_file=mycustom.dtb
      extra_os_cmdline=isolcpus=3,4
      another_value
    `,
		reason: 'Could not read extra_uEnv entry: another_value',
	},
];

const MATCH_TESTS = [
	{ type: 'astro-tx2', supported: true },
	{ type: 'blackboard-tx2', supported: true },
	{ type: 'jetson-tx2', supported: true },
	{ type: 'n310-tx2', supported: true },
	{ type: 'n510-tx2', supported: true },
	{ type: 'orbitty-tx2', supported: true },
	{ type: 'spacely-tx2', supported: true },
	{ type: 'srd3-tx2', supported: true },
	{ type: 'jetson-nano', supported: true },
	{ type: 'jetson-nano-emmc', supported: true },
	{ type: 'jn30b-nano', supported: true },
	{ type: 'photon-nano', supported: true },
	{ type: 'intel-nuc', supported: false },
	{ type: 'raspberry', supported: false },
	{ type: 'fincm3', supported: false },
	{ type: 'asus-tinker-board', supported: false },
	{ type: 'nano-board', supported: false },
	{ type: 'jetson-nano-2gb-devkit', supported: true },
	{ type: 'jetson-nano-2gb-devkit-emmc', supported: false },
	{ type: 'tx2-tx2-device', supported: false },
	{ type: 'jetson-tx2-nx-devkit', supported: true },
	{ type: 'photon-tx2-nx', supported: true },
	{ type: 'jetson-xavier-nx-devkit', supported: false },
	{ type: 'jetson-agx-orin-devkit', supported: true },
	{ type: 'jetson-agx-orin', supported: false },
	{ type: 'jetson-orin-nx-xavier-nx-devkit', supported: true },
	{ type: 'cti-orin-nx-custom-carrier', supported: true },
	{ type: 'jetson-orin-agx-nx-xavier-nx-devkit', supported: false },
	{ type: 'jetson-orin-nano-devkit-nvme', supported: true },
	{ type: 'jetson-orin-agx-nano-devkit-nvme', supported: false },
	{ type: 'photon-xavier-nx', supported: false },
	{ type: 'imx8m-var-dart', supported: true },
	{ type: 'imx8mm-var-dart', supported: true },
	{ type: 'imx8mm-var-dart-nrt', supported: true },
	{ type: 'imx8mm-var-dart-plt', supported: true },
	{ type: 'imx8mm-var-som', supported: true },
	{ type: 'imx8m-var-som', supported: false },
	{ type: 'imx6ul-var-dart', supported: false },
];