import { expect } from 'chai';
import type { SinonStub } from 'sinon';
import { stub } from 'sinon';
import { promises as fs } from 'fs';
import systeminformation from 'systeminformation';

import * as fsUtils from '~/lib/fs-utils';
import * as sysInfo from '~/lib/system-info';

describe('System information', () => {
	before(() => {
		stub(systeminformation, 'fsSize');
		stub(systeminformation, 'mem').resolves(mockMemory);
		stub(systeminformation, 'currentLoad').resolves(mockCPU.load);
		stub(systeminformation, 'cpuTemperature').resolves(mockCPU.temp);
		stub(fs, 'readFile').resolves(mockCPU.idBuffer);
		stub(fsUtils, 'exec');
	});

	after(() => {
		(systeminformation.fsSize as SinonStub).restore();
		(systeminformation.mem as SinonStub).restore();
		(systeminformation.currentLoad as SinonStub).restore();
		(systeminformation.cpuTemperature as SinonStub).restore();
		(fs.readFile as SinonStub).restore();
		(fsUtils.exec as SinonStub).restore();
	});

	describe('isSignificantChange', () => {
		it('should correctly filter cpu usage', () => {
			expect(sysInfo.isSignificantChange('cpu_usage', 21, 20)).to.be.false;
			expect(sysInfo.isSignificantChange('cpu_usage', 10, 20)).to.be.true;
		});

		it('should correctly filter cpu temperature', () => {
			expect(sysInfo.isSignificantChange('cpu_temp', 21, 22)).to.be.false;
			expect(sysInfo.isSignificantChange('cpu_temp', 10, 20)).to.be.true;
		});

		it('should correctly filter memory usage', () => {
			expect(sysInfo.isSignificantChange('memory_usage', 21, 22)).to.be.false;
			expect(sysInfo.isSignificantChange('memory_usage', 10, 20)).to.be.true;
		});

		it("should not filter if we didn't have a past value", () => {
			expect(sysInfo.isSignificantChange('cpu_usage', undefined, 22)).to.be
				.true;
			expect(sysInfo.isSignificantChange('cpu_temp', undefined, 10)).to.be.true;
			expect(sysInfo.isSignificantChange('memory_usage', undefined, 5)).to.be
				.true;
		});

		it('should not filter if the current value is null', () => {
			// When the current value is null, we're sending a null patch to the
			// API in response to setting DISABLE_HARDWARE_METRICS to true, so
			// we need to include null for all values. None of the individual metrics
			// in systemMetrics return null (only number/undefined), so the only
			// reason for current to be null is when a null patch is happening.
			expect(sysInfo.isSignificantChange('cpu_usage', 15, null as any)).to.be
				.true;
			expect(sysInfo.isSignificantChange('cpu_temp', 55, null as any)).to.be
				.true;
			expect(sysInfo.isSignificantChange('memory_usage', 760, null as any)).to
				.be.true;
		});

		it('should not filter if the current value is null', () => {
			// When the current value is null, we're sending a null patch to the
			// API in response to setting HARDWARE_METRICS to false, so
			// we need to include null for all values. None of the individual metrics
			// in systemMetrics return null (only number/undefined), so the only
			// reason for current to be null is when a null patch is happening.
			expect(sysInfo.isSignificantChange('cpu_usage', 15, null as any)).to.be
				.true;
			expect(sysInfo.isSignificantChange('cpu_temp', 55, null as any)).to.be
				.true;
			expect(sysInfo.isSignificantChange('memory_usage', 760, null as any)).to
				.be.true;
		});
	});

	describe('CPU information', () => {
		it('gets CPU usage', async () => {
			const cpuUsage = await sysInfo.getCpuUsage();
			// Make sure it is a whole number
			expect(cpuUsage % 1).to.equal(0);
			// Make sure it's the right number given the mocked data
			expect(cpuUsage).to.equal(1);
		});

		it('gets CPU temp', async () => {
			const tempInfo = await sysInfo.getCpuTemp();
			// Make sure it is a whole number
			expect(tempInfo % 1).to.equal(0);
			// Make sure it's the right number given the mocked data
			expect(tempInfo).to.equal(51);
		});
	});

	describe('baseboard information', () => {
		afterEach(() => {
			(fs.readFile as SinonStub).reset();
			(fsUtils.exec as SinonStub).reset();
		});

		// Do these two tests first so the dmidecode call is not memoized yet
		it('returns undefined system model if dmidecode throws', async () => {
			(fs.readFile as SinonStub).rejects('Not found');
			(fsUtils.exec as SinonStub).rejects('Something bad happened');
			const systemModel = await sysInfo.getSystemModel();
			expect(systemModel).to.be.undefined;
		});

		it('returns undefined system ID if dmidecode throws', async () => {
			(fs.readFile as SinonStub).rejects('Not found');
			(fsUtils.exec as SinonStub).rejects('Something bad happened');
			const systemId = await sysInfo.getSystemId();
			expect(systemId).to.be.undefined;
		});

		it('gets system ID', async () => {
			(fs.readFile as SinonStub).resolves(mockCPU.idBuffer);
			const cpuId = await sysInfo.getSystemId();
			expect(cpuId).to.equal('1000000001b93f3f');
		});

		it('gets system ID from dmidecode if /proc/device-tree/serial-number is not available', async () => {
			(fs.readFile as SinonStub).rejects('Not found');
			(fsUtils.exec as SinonStub).resolves({
				stdout: mockCPU.dmidecode,
			});
			const cpuId = await sysInfo.getSystemId();
			expect(cpuId).to.equal('gebn94600pww');
		});

		it('gets system model', async () => {
			(fs.readFile as SinonStub).resolves('Raspberry PI 4');
			const systemModel = await sysInfo.getSystemModel();
			expect(systemModel).to.equal('Raspberry PI 4');
		});

		it('gets system model from dmidecode if /proc/device-tree/model is not available', async () => {
			(fs.readFile as SinonStub).rejects('Not found');
			(fsUtils.exec as SinonStub).resolves({
				stdout: mockCPU.dmidecode,
			});
			const systemModel = await sysInfo.getSystemModel();
			expect(systemModel).to.equal('Intel Corporation NUC7i5BNB');
		});
	});

	describe('getMemoryInformation', async () => {
		it('should return the correct value for memory usage', async () => {
			const memoryInfo = await sysInfo.getMemoryInformation();
			expect(memoryInfo).to.deep.equal({
				total: sysInfo.bytesToMb(mockMemory.total),
				used: sysInfo.bytesToMb(
					mockMemory.total -
						mockMemory.free -
						(mockMemory.cached + mockMemory.buffers),
				),
			});
		});
	});

	describe('getStorageInfo', async () => {
		it('should return info on /data mount', async () => {
			(systeminformation.fsSize as SinonStub).resolves(mockFS);
			const storageInfo = await sysInfo.getStorageInfo();
			expect(storageInfo).to.deep.equal({
				blockDevice: '/dev/mmcblk0p6',
				storageUsed: 1118,
				storageTotal: 29023,
			});
		});

		it('should handle no /data mount', async () => {
			(systeminformation.fsSize as SinonStub).resolves([]);
			const storageInfo = await sysInfo.getStorageInfo();
			expect(storageInfo).to.deep.equal({
				blockDevice: '',
				storageUsed: undefined,
				storageTotal: undefined,
			});
		});
	});

	describe('undervoltageDetected', () => {
		it('should detect undervoltage', async () => {
			(fsUtils.exec as SinonStub).resolves({
				stdout: Buffer.from(
					'[58611.126996] Under-voltage detected! (0x00050005)',
				),
				stderr: Buffer.from(''),
			});
			expect(await sysInfo.undervoltageDetected()).to.be.true;
			(fsUtils.exec as SinonStub).resolves({
				stdout: Buffer.from('[569378.450066] eth0: renamed from veth3aa11ca'),
				stderr: Buffer.from(''),
			});
			expect(await sysInfo.undervoltageDetected()).to.be.false;
		});
	});

	describe('dmidecode', () => {
		it('parses dmidecode output into json', async () => {
			(fsUtils.exec as SinonStub).resolves({
				stdout: mockCPU.dmidecode,
			});

			expect(await sysInfo.dmidecode('baseboard')).to.deep.equal([
				{
					type: 'Base Board Information',
					values: {
						Manufacturer: 'Intel Corporation',
						'Product Name': 'NUC7i5BNB',
						Version: 'J31144-313',
						'Serial Number': 'GEBN94600PWW',
						'Location In Chassis': 'Default string',
						'Chassis Handle': '0x0003',
						Type: 'Motherboard',
						'Contained Object Handles': '0',
					},
				},
				{
					type: 'On Board Device 1 Information',
					values: {
						Type: 'Sound',
						Status: 'Enabled',
						Description: 'Realtek High Definition Audio Device',
					},
				},
				{
					type: 'Onboard Device',
					values: {
						'Reference Designation': 'Onboard - Other',
						Type: 'Other',
						Status: 'Enabled',
						'Type Instance': '1',
						'Bus Address': '0000',
					},
				},
			]);

			// Reset the stub
			(fsUtils.exec as SinonStub).reset();
		});
	});
});

const mockCPU = {
	temp: {
		main: 50.634,
		cores: [],
		max: 50.634,
		socket: [],
	},
	load: {
		avgLoad: 0.6,
		currentLoad: 1.4602487831260142,
		currentLoadUser: 0.7301243915630071,
		currentLoadSystem: 0.7301243915630071,
		currentLoadNice: 0,
		currentLoadIdle: 98.53975121687398,
		currentLoadIrq: 0,
		currentLoadSteal: 0,
		currentLoadGuest: 0,
		rawCurrentLoad: 5400,
		rawCurrentLoadUser: 2700,
		rawCurrentLoadSystem: 2700,
		rawCurrentLoadNice: 0,
		rawCurrentLoadIdle: 364400,
		rawCurrentLoadIrq: 0,
		rawCurrentLoadSteal: 0,
		rawCurrentLoadGuest: 0,
		cpus: [
			{
				load: 1.8660812294182216,
				loadUser: 0.7683863885839737,
				loadSystem: 1.0976948408342482,
				loadNice: 0,
				loadIdle: 98.13391877058177,
				loadIrq: 0,
				loadSteal: 0,
				loadGuest: 0,
				rawLoad: 1700,
				rawLoadUser: 700,
				rawLoadSystem: 1000,
				rawLoadNice: 0,
				rawLoadIdle: 89400,
				rawLoadIrq: 0,
				rawLoadSteal: 0,
				rawLoadGuest: 0,
			},
			{
				load: 1.7204301075268817,
				loadUser: 0.8602150537634409,
				loadSystem: 0.8602150537634409,
				loadNice: 0,
				loadIdle: 98.27956989247312,
				loadIrq: 0,
				loadSteal: 0,
				loadGuest: 0,
				rawLoad: 1600,
				rawLoadUser: 800,
				rawLoadSystem: 800,
				rawLoadNice: 0,
				rawLoadIdle: 91400,
				rawLoadIrq: 0,
				rawLoadSteal: 0,
				rawLoadGuest: 0,
			},
			{
				load: 1.186623516720604,
				loadUser: 0.9708737864077669,
				loadSystem: 0.2157497303128371,
				loadNice: 0,
				loadIdle: 98.8133764832794,
				loadIrq: 0,
				loadSteal: 0,
				loadGuest: 0,
				rawLoad: 1100,
				rawLoadUser: 900,
				rawLoadSystem: 200,
				rawLoadNice: 0,
				rawLoadIdle: 91600,
				rawLoadIrq: 0,
				rawLoadSteal: 0,
				rawLoadGuest: 0,
			},
			{
				load: 1.0752688172043012,
				loadUser: 0.3225806451612903,
				loadSystem: 0.7526881720430108,
				loadNice: 0,
				loadIdle: 98.9247311827957,
				loadIrq: 0,
				loadSteal: 0,
				loadGuest: 0,
				rawLoad: 1000,
				rawLoadUser: 300,
				rawLoadSystem: 700,
				rawLoadNice: 0,
				rawLoadIdle: 92000,
				rawLoadIrq: 0,
				rawLoadSteal: 0,
				rawLoadGuest: 0,
			},
		],
	},
	idBuffer: Buffer.from('1000000001b93f3f'),
	dmidecode: Buffer.from(`# dmidecode 3.3
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.

Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
        Manufacturer: Intel Corporation
        Product Name: NUC7i5BNB
        Version: J31144-313
        Serial Number: GEBN94600PWW
        Asset Tag:
        Features:
                Board is a hosting board
                Board is replaceable
        Location In Chassis: Default string
        Chassis Handle: 0x0003
        Type: Motherboard
        Contained Object Handles: 0

Handle 0x000F, DMI type 10, 20 bytes
On Board Device 1 Information
        Type: Video
        Status: Enabled
        Description:  Intel(R) HD Graphics Device
On Board Device 2 Information
        Type: Ethernet
        Status: Enabled
        Description:  Intel(R) I219-V Gigabit Network Device
On Board Device 3 Information
        Type: Sound
        Status: Enabled
        Description:  Realtek High Definition Audio Device

Handle 0x003F, DMI type 41, 11 bytes
Onboard Device
        Reference Designation: Onboard - Other
        Type: Other
        Status: Enabled
        Type Instance: 1
        Bus Address: 0000:00:00.0
				`),
};
const mockFS = [
	{
		fs: 'overlay',
		type: 'overlay',
		size: 30433308672,
		used: 1172959232,
		available: 27684696064,
		use: 4.06,
		mount: '/',
	},
	{
		fs: '/dev/mmcblk0p6',
		type: 'ext4',
		size: 30433308672,
		used: 1172959232,
		available: 27684696064,
		use: 4.06,
		mount: '/data',
	},
	{
		fs: '/dev/mmcblk0p1',
		type: 'vfat',
		size: 41281536,
		used: 7219200,
		available: 34062336,
		use: 17.49,
		mount: '/boot/config.json',
	},
	{
		fs: '/dev/disk/by-state/resin-state',
		type: 'ext4',
		size: 19254272,
		used: 403456,
		available: 17383424,
		use: 2.27,
		mount: '/mnt/root/mnt/state',
	},
	{
		fs: '/dev/disk/by-uuid/ba1eadef-4660-4b03-9e71-9f33257f292c',
		type: 'ext4',
		size: 313541632,
		used: 308860928,
		available: 0,
		use: 100,
		mount: '/mnt/root/mnt/sysroot/active',
	},
	{
		fs: '/dev/mmcblk0p2',
		type: 'ext4',
		size: 313541632,
		used: 299599872,
		available: 0,
		use: 100,
		mount: '/mnt/root/mnt/sysroot/inactive',
	},
];
const mockMemory = {
	total: 4032724992,
	free: 2182356992,
	used: 1850368000,
	active: 459481088,
	available: 3573243904,
	buffers: 186269696,
	cached: 1055621120,
	slab: 252219392,
	buffcache: 1494110208,
	swaptotal: 2016358400,
	swapused: 0,
	swapfree: 2016358400,
	writeback: 0,
	dirty: 0,
};
