import { promises as fs } from 'fs';
import type { TestFs } from 'mocha-pod';
import { testfs } from 'mocha-pod';

import { expect } from 'chai';
import * as hostUtils from '~/lib/host-utils';
import { SplashImage } from '~/src/config/backends/splash-image';
import log from '~/lib/supervisor-console';

describe('config/splash-image', () => {
	const backend = new SplashImage();
	const defaultLogo =
		'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
	const logo =
		'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
	const uri = `data:image/png;base64,${logo}`;

	describe('initialise', () => {
		let tfs: TestFs.Enabled;

		beforeEach(async () => {
			tfs = await testfs(
				{
					[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
						defaultLogo,
						'base64',
					),
				},
				{ cleanup: [hostUtils.pathOnBoot('splash/balena-logo-default.png')] },
			).enable();
		});

		afterEach(async () => {
			await tfs.restore();
		});

		it('should make a copy of the existing boot image on initialise if not yet created', async () => {
			await expect(
				fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
				'logo copy should not exist before first initialization',
			).to.be.rejected;

			// Do the initialization
			await backend.initialise();

			// The copy should exist after the test and equal defaultLogo
			await expect(
				fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
				'logo copy should exist after initialization',
			).to.not.be.rejected;

			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo-default.png'),
					'base64',
				),
			).to.equal(defaultLogo);
		});

		it('should skip initialization if the default image already exists', async () => {
			// Write a different logo as default
			await fs.writeFile(
				hostUtils.pathOnBoot('splash/balena-logo-default.png'),
				Buffer.from(logo, 'base64'),
			);
			// Do the initialization
			await backend.initialise();

			// The copy should exist after the test and be equal to logo (it should not) have
			// been changed
			await expect(
				fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
				'logo copy still exists after initialization',
			).to.not.be.rejected;

			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo-default.png'),
					'base64',
				),
			).to.equal(logo);
		});

		it('should warn about failed initialization if there is no default image on the device', async () => {
			await fs.unlink(hostUtils.pathOnBoot('splash/balena-logo.png'));

			// Do the initialization
			await backend.initialise();

			expect(log.warn).to.be.calledOnceWith(
				'Could not initialise splash image backend',
			);
		});
	});

	describe('getBootConfig', () => {
		it('should return an empty object if the current logo matches the default logo', async () => {
			const tfs = await testfs({
				// Both the logo and the copy resolve to the same value
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					logo,
					'base64',
				),
			}).enable();

			// The default logo resolves to the same value as the current logo
			expect(await backend.getBootConfig()).to.deep.equal({});

			await tfs.restore();
		});

		it('should read the splash image from resin-logo.png if available', async () => {
			const tfs = await testfs({
				// resin logo and balena-logo-default are different which means the current logo
				// is a custom image
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			expect(await backend.getBootConfig()).to.deep.equal({
				image: uri,
			});

			await tfs.restore();
		});

		it('should read the splash image from balena-logo.png if available', async () => {
			const tfs = await testfs({
				// balena-logo and balena-logo-default are different which means the current logo
				// is a custom image
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			expect(await backend.getBootConfig()).to.deep.equal({
				image: uri,
			});

			await tfs.restore();
		});

		it('should read the splash image from balena-logo.png even if resin-logo.png exists', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				// resin-logo has the same value as default, but it is ignored since balena-logo exists
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			expect(await backend.getBootConfig()).to.deep.equal({
				// balena-logo is the value read
				image: uri,
			});

			await tfs.restore();
		});

		it('should catch readDir errors', async () => {
			// Remove the directory before the tests to cause getBootConfig to fail
			await fs
				.rm(hostUtils.pathOnBoot('splash'), { recursive: true, force: true })
				.catch(() => {
					/* noop */
				});

			expect(await backend.getBootConfig()).to.deep.equal({});
			expect(log.warn).to.be.calledOnceWith('Failed to read splash image:');
		});

		it('should catch readFile errors', async () => {
			await expect(
				fs.access(hostUtils.pathOnBoot('splash/balena-logo.png')),
				'logo does not exist before getting boot config',
			).to.be.rejected;
			await expect(
				fs.access(hostUtils.pathOnBoot('splash/resin-logo.png')),
				'logo does not exist before getting boot config',
			).to.be.rejected;

			expect(await backend.getBootConfig()).to.deep.equal({});
			expect(log.warn).to.be.calledOnceWith('Failed to read splash image:');
		});
	});

	describe('setBootConfig', () => {
		it('should write the given image to resin-logo.png if set', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			await backend.setBootConfig({ image: uri });

			// Since resin-logo already exists we use that to write
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/resin-logo.png'),
					'base64',
				),
			).to.equal(logo);

			await tfs.restore();
		});

		it('should write the given image to balena-logo.png if set', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			await backend.setBootConfig({ image: uri });

			// Resin logo should not have changed
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/resin-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

			// balena-logo is used as the correct location
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(logo);

			await tfs.restore();
		});

		it('should accept just a base64 as an image', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// We use just the base64 image instead of the data uri
			await backend.setBootConfig({ image: logo });

			// The file should have changed
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(logo);

			await tfs.restore();
		});

		it('should restore balena-logo.png if image arg is unset', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// setBootConfig with an empty object should delete the iamge
			await backend.setBootConfig({});

			// The default should have been reverted to the default value
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

			await tfs.restore();
		});

		it('should restore resin-logo.png if image arg is unset', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// setBootConfig with an empty object should delete the iamge
			await backend.setBootConfig({});

			// The default should have been reverted to the default value
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/resin-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

			await tfs.restore();
		});

		it('should restore balena-logo.png if image arg is empty', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					logo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// setBootConfig with an empty image should also restore the default
			await backend.setBootConfig({ image: '' });

			// The default should have been reverted to the default value
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

			await tfs.restore();
		});

		// TODO: note that ignoring a value on the backend will cause the supervisor
		// to go into a loop trying to apply target state, as the current will never match
		// the target. The image needs to be validated cloud side, and as a last line of defense,
		// when receiving the target state
		it('should ignore the value if arg is not a valid base64 string', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// We pass something that is not an image
			await backend.setBootConfig({ image: 'somestring' });

			// The file should NOT have changed
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

			await tfs.restore();
		});

		it('should ignore the value if image is not a valid PNG file', async () => {
			const tfs = await testfs({
				[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
				[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
					defaultLogo,
					'base64',
				),
			}).enable();

			// We pass something that is not a PNG
			await backend.setBootConfig({ image: 'aGVsbG8=' });

			// The file should NOT have changed
			expect(
				await fs.readFile(
					hostUtils.pathOnBoot('splash/balena-logo.png'),
					'base64',
				),
			).to.equal(defaultLogo);

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

	describe('isBootConfigVar', () => {
		it('Accepts any case variable names', () => {
			expect(backend.isBootConfigVar('HOST_SPLASH_IMAGE')).to.be.true;
			expect(backend.isBootConfigVar('HOST_SPLASH_image')).to.be.true;
			expect(backend.isBootConfigVar('HOST_SPLASH_Image')).to.be.true;
			expect(backend.isBootConfigVar('HOST_SPLASH_ImAgE')).to.be.true;
		});
	});
});
