import { expect } from 'chai';
import type * as express from 'express';
import type { SinonStub } from 'sinon';
import { stub } from 'sinon';
import request from 'supertest';

import * as config from '~/src/config';
import * as db from '~/src/db';
import * as hostConfig from '~/src/host-config';
import type { Service } from '~/src/compose/service';
import * as apiKeys from '~/lib/api-keys';
import * as deviceApi from '~/src/device-api';
import * as actions from '~/src/device-api/actions';
import * as v1 from '~/src/device-api/v1';
import {
	UpdatesLockedError,
	NotFoundError,
	BadRequestError,
} from '~/lib/errors';
import log from '~/lib/supervisor-console';

// All routes that require Authorization are integration tests due to
// the api-key module relying on the database.
describe('device-api/v1', () => {
	let api: express.Application;

	before(async () => {
		await config.initialized();

		// `api` is a private property on SupervisorAPI but
		// passing it directly to supertest is easier than
		// setting up an API listen port & timeout
		api = new deviceApi.SupervisorAPI({
			routers: [v1.router],
			healthchecks: [],
			// @ts-expect-error extract private variable for testing
		}).api;
	});

	describe('GET /v1/healthy', () => {
		after(() => {
			api = new deviceApi.SupervisorAPI({
				routers: [v1.router],
				healthchecks: [],
				// @ts-expect-error extract private variable for testing
			}).api;
		});

		it('responds with 200 because all healthchecks pass', async () => {
			api = new deviceApi.SupervisorAPI({
				routers: [v1.router],
				healthchecks: [stub().resolves(true), stub().resolves(true)],
				// @ts-expect-error extract private variable for testing
			}).api;
			await request(api).get('/v1/healthy').expect(200);
		});

		it('responds with 500 because some healthchecks did not pass', async () => {
			api = new deviceApi.SupervisorAPI({
				routers: [v1.router],
				healthchecks: [stub().resolves(false), stub().resolves(true)],
				// @ts-expect-error extract private variable for testing
			}).api;
			await request(api).get('/v1/healthy').expect(500);
		});
	});

	describe('POST /v1/blink', () => {
		// Actions are tested elsewhere so we can stub the dependency here
		before(() => stub(actions, 'identify'));
		after(() => (actions.identify as SinonStub).restore());

		it('responds with 200', async () => {
			await request(api)
				.post('/v1/blink')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
		});
	});

	describe('POST /v1/regenerate-api-key', () => {
		// Actions are tested elsewhere so we can stub the dependency here
		beforeEach(() => stub(actions, 'regenerateKey'));
		afterEach(() => (actions.regenerateKey as SinonStub).restore());

		it('responds with 200 and valid new API key', async () => {
			const oldKey = await apiKeys.getGlobalApiKey();
			const newKey = 'my_new_key';
			(actions.regenerateKey as SinonStub).resolves(newKey);

			await request(api)
				.post('/v1/regenerate-api-key')
				.set('Authorization', `Bearer ${oldKey}`)
				.expect(200)
				.then((response) => {
					expect(response.text).to.match(new RegExp(newKey));
				});
		});

		it('responds with 503 if regenerate was unsuccessful', async () => {
			const oldKey = await apiKeys.getGlobalApiKey();
			(actions.regenerateKey as SinonStub).throws(new Error());

			await request(api)
				.post('/v1/regenerate-api-key')
				.set('Authorization', `Bearer ${oldKey}`)
				.expect(503);
		});
	});

	describe('POST /v1/restart', () => {
		// Actions are tested elsewhere so we can stub the dependency here
		let doRestartStub: SinonStub;
		beforeEach(() => {
			doRestartStub = stub(actions, 'doRestart').resolves();
		});
		afterEach(async () => {
			doRestartStub.restore();
			// Remove all scoped API keys between tests
			await db.models('apiSecret').whereNot({ appId: 0 }).del();
		});

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/restart')
				.send({ appId: 1234567, force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doRestartStub).to.have.been.calledWith(1234567, false);
			doRestartStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/restart')
				.send({ appId: 7654321, force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doRestartStub).to.have.been.calledWith(7654321, true);
			doRestartStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/restart')
				.send({ appId: 7654321 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doRestartStub).to.have.been.calledWith(7654321, false);
		});

		it('responds with 400 if appId is missing', async () => {
			await request(api)
				.post('/v1/restart')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it("responds with 401 if caller's API key is not in scope of appId", async () => {
			const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
			await request(api)
				.post('/v1/restart')
				.send({ appId: 7654321 })
				.set('Authorization', `Bearer ${scopedKey}`)
				.expect(401);
		});

		it('responds with 200 if restart succeeded', async () => {
			await request(api)
				.post('/v1/restart')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
		});

		it('responds with 423 if there are update locks', async () => {
			doRestartStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/restart')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 503 for other errors that occur during restart', async () => {
			doRestartStub.throws(new Error());
			await request(api)
				.post('/v1/restart')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('POST /v1/purge', () => {
		let doPurgeStub: SinonStub;
		beforeEach(() => {
			doPurgeStub = stub(actions, 'doPurge').resolves();
		});
		afterEach(async () => {
			doPurgeStub.restore();
			// Remove all scoped API keys between tests
			await db.models('apiSecret').whereNot({ appId: 0 }).del();
		});

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/purge')
				.send({ appId: 1234567, force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doPurgeStub).to.have.been.calledWith(1234567, false);
			doPurgeStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/purge')
				.send({ appId: 7654321, force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doPurgeStub).to.have.been.calledWith(7654321, true);
			doPurgeStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/purge')
				.send({ appId: 7654321 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(doPurgeStub).to.have.been.calledWith(7654321, false);
		});

		it('responds with 400 if appId is missing', async () => {
			await request(api)
				.post('/v1/purge')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it("responds with 401 if caller's API key is not in scope of appId", async () => {
			const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
			await request(api)
				.post('/v1/purge')
				.send({ appId: 7654321 })
				.set('Authorization', `Bearer ${scopedKey}`)
				.expect(401);
		});

		it('responds with 200 if purge succeeded', async () => {
			await request(api)
				.post('/v1/purge')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
		});

		it('responds with 423 if there are update locks', async () => {
			doPurgeStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/purge')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 503 for other errors that occur during purge', async () => {
			doPurgeStub.throws(new Error());
			await request(api)
				.post('/v1/purge')
				.send({ appId: 1234567 })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('POST /v1/apps/:appId/stop', () => {
		let executeServiceActionStub: SinonStub;
		let getLegacyServiceStub: SinonStub;
		beforeEach(() => {
			executeServiceActionStub = stub(
				actions,
				'executeServiceAction',
			).resolves();
			getLegacyServiceStub = stub(actions, 'getLegacyService').resolves({
				containerId: 'abcdef',
			} as Service);
		});
		afterEach(async () => {
			executeServiceActionStub.restore();
			getLegacyServiceStub.restore();
			// Remove all scoped API keys between tests
			await db.models('apiSecret').whereNot({ appId: 0 }).del();
		});

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/apps/1234567/stop')
				.send({ force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'stop',
				appId: 1234567,
				force: false,
				isLegacy: true,
			});
			executeServiceActionStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/apps/7654321/stop')
				.send({ force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'stop',
				appId: 7654321,
				force: true,
				isLegacy: true,
			});
			executeServiceActionStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/apps/7654321/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'stop',
				appId: 7654321,
				force: false,
				isLegacy: true,
			});
		});

		it("responds with 401 if caller's API key is not in scope of appId", async () => {
			const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
			await request(api)
				.post('/v1/apps/7654321/stop')
				.set('Authorization', `Bearer ${scopedKey}`)
				.expect(401);
		});

		it('responds with 200 and containerId if service stop succeeded if service stop succeeded', async () => {
			await request(api)
				.post('/v1/apps/1234567/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200, { containerId: 'abcdef' });
		});

		it('responds with 404 if app or service not found', async () => {
			executeServiceActionStub.throws(new NotFoundError());
			await request(api)
				.post('/v1/apps/1234567/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(404);
		});

		it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => {
			await request(api)
				.post('/v1/apps/badAppId/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);

			executeServiceActionStub.throws(new BadRequestError());
			await request(api)
				.post('/v1/apps/1234567/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it('responds with 423 if there are update locks', async () => {
			executeServiceActionStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/apps/1234567/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 503 for other errors that occur during service stop', async () => {
			executeServiceActionStub.throws(new Error());
			await request(api)
				.post('/v1/apps/1234567/stop')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('POST /v1/apps/:appId/start', () => {
		let executeServiceActionStub: SinonStub;
		let getLegacyServiceStub: SinonStub;
		beforeEach(() => {
			executeServiceActionStub = stub(
				actions,
				'executeServiceAction',
			).resolves();
			getLegacyServiceStub = stub(actions, 'getLegacyService').resolves({
				containerId: 'abcdef',
			} as Service);
		});
		afterEach(async () => {
			executeServiceActionStub.restore();
			getLegacyServiceStub.restore();
			// Remove all scoped API keys between tests
			await db.models('apiSecret').whereNot({ appId: 0 }).del();
		});

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/apps/1234567/start')
				.send({ force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'start',
				appId: 1234567,
				force: false,
				isLegacy: true,
			});
			executeServiceActionStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/apps/7654321/start')
				.send({ force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'start',
				appId: 7654321,
				force: true,
				isLegacy: true,
			});
			executeServiceActionStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/apps/7654321/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
			expect(executeServiceActionStub).to.have.been.calledWith({
				action: 'start',
				appId: 7654321,
				force: false,
				isLegacy: true,
			});
		});

		it("responds with 401 if caller's API key is not in scope of appId", async () => {
			const scopedKey = await apiKeys.generateScopedKey(1234567, 'main');
			await request(api)
				.post('/v1/apps/7654321/start')
				.set('Authorization', `Bearer ${scopedKey}`)
				.expect(401);
		});

		it('responds with 200 and containerId if service start succeeded', async () => {
			await request(api)
				.post('/v1/apps/1234567/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200, { containerId: 'abcdef' });
		});

		it('responds with 404 if app or service not found', async () => {
			executeServiceActionStub.throws(new NotFoundError());
			await request(api)
				.post('/v1/apps/1234567/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(404);
		});

		it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => {
			await request(api)
				.post('/v1/apps/badAppId/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);

			executeServiceActionStub.throws(new BadRequestError());
			await request(api)
				.post('/v1/apps/1234567/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it('responds with 423 if there are update locks', async () => {
			executeServiceActionStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/apps/1234567/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 503 for other errors that occur during service start', async () => {
			executeServiceActionStub.throws(new Error());
			await request(api)
				.post('/v1/apps/1234567/start')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('POST /v1/reboot', () => {
		let executeDeviceActionStub: SinonStub;
		beforeEach(() => {
			executeDeviceActionStub = stub(actions, 'executeDeviceAction').resolves();
		});
		afterEach(async () => executeDeviceActionStub.restore());

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/reboot')
				.send({ force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'reboot',
				},
				false,
			);
			executeDeviceActionStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/reboot')
				.send({ force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'reboot',
				},
				true,
			);
			executeDeviceActionStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/reboot')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'reboot',
				},
				false,
			);
		});

		it('responds with 202 if request successful', async () => {
			await request(api)
				.post('/v1/reboot')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(202);
		});

		it('responds with 423 if there are update locks', async () => {
			executeDeviceActionStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/reboot')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 500 for other errors that occur during reboot', async () => {
			executeDeviceActionStub.throws(new Error());
			await request(api)
				.post('/v1/reboot')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(500);
		});
	});

	describe('POST /v1/shutdown', () => {
		let executeDeviceActionStub: SinonStub;
		beforeEach(() => {
			executeDeviceActionStub = stub(actions, 'executeDeviceAction').resolves();
		});
		afterEach(async () => executeDeviceActionStub.restore());

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/shutdown')
				.send({ force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'shutdown',
				},
				false,
			);
			executeDeviceActionStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/shutdown')
				.send({ force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'shutdown',
				},
				true,
			);
			executeDeviceActionStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/shutdown')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(executeDeviceActionStub).to.have.been.calledWith(
				{
					action: 'shutdown',
				},
				false,
			);
		});

		it('responds with 202 if request successful', async () => {
			await request(api)
				.post('/v1/shutdown')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(202);
		});

		it('responds with 423 if there are update locks', async () => {
			executeDeviceActionStub.throws(new UpdatesLockedError());
			await request(api)
				.post('/v1/shutdown')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 500 for other errors that occur during shutdown', async () => {
			executeDeviceActionStub.throws(new Error());
			await request(api)
				.post('/v1/shutdown')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(500);
		});
	});

	describe('POST /v1/update', () => {
		let updateTargetStub: SinonStub;
		beforeEach(() => {
			updateTargetStub = stub(actions, 'updateTarget');
		});
		afterEach(async () => updateTargetStub.restore());

		it('validates data from request body', async () => {
			// Parses force: false
			await request(api)
				.post('/v1/update')
				.send({ force: false })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(updateTargetStub.lastCall.firstArg).to.be.false;
			updateTargetStub.resetHistory();

			// Parses force: true
			await request(api)
				.post('/v1/update')
				.send({ force: true })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(updateTargetStub.lastCall.firstArg).to.be.true;
			updateTargetStub.resetHistory();

			// Defaults to force: false
			await request(api)
				.post('/v1/update')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(updateTargetStub.lastCall.firstArg).to.be.false;
		});

		it('responds with 204 if update triggered', async () => {
			updateTargetStub.returns(true);
			await request(api)
				.post('/v1/update')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(204);
		});

		it('responds with 202 if update not triggered', async () => {
			updateTargetStub.returns(false);
			await request(api)
				.post('/v1/update')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(202);
		});
	});

	describe('GET /v1/apps/:appId', () => {
		let getSingleContainerAppStub: SinonStub;
		beforeEach(() => {
			getSingleContainerAppStub = stub(
				actions,
				'getSingleContainerApp',
			).resolves({} as any);
		});
		afterEach(async () => {
			getSingleContainerAppStub.restore();
			// Remove all scoped API keys between tests
			await db.models('apiSecret').whereNot({ appId: 0 }).del();
		});

		it('validates data from request body', async () => {
			await request(api)
				.get('/v1/apps/1234567')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`);
			expect(getSingleContainerAppStub).to.have.been.calledWith(1234567);
		});

		it('responds with 200 if request successful', async () => {
			await request(api)
				.get('/v1/apps/1234567')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200, {});
		});

		it('responds with 400 if invalid appId parameter', async () => {
			await request(api)
				.get('/v1/apps/badAppId')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it('responds with 400 if action throws BadRequestError', async () => {
			getSingleContainerAppStub.throws(new BadRequestError());
			await request(api)
				.get('/v1/apps/1234567')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it("responds with 401 if caller's API key is not in scope of appId", async () => {
			const scopedKey = await apiKeys.generateScopedKey(7654321, 'main');
			await request(api)
				.get('/v1/apps/1234567')
				.set('Authorization', `Bearer ${scopedKey}`)
				.expect(401);
		});

		it('responds with 503 for other errors that occur during request', async () => {
			getSingleContainerAppStub.throws(new Error());
			await request(api)
				.get('/v1/apps/1234567')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('GET /v1/device', () => {
		let getLegacyDeviceStateStub: SinonStub;
		beforeEach(() => {
			getLegacyDeviceStateStub = stub(actions, 'getLegacyDeviceState');
		});
		afterEach(() => getLegacyDeviceStateStub.restore());

		it('responds with 200 and legacy device state', async () => {
			getLegacyDeviceStateStub.resolves({ test_state: 'Success' });
			await request(api)
				.get('/v1/device')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200, { test_state: 'Success' });
		});

		it('responds with 503 for other errors that occur during request', async () => {
			getLegacyDeviceStateStub.throws(new Error());
			await request(api)
				.get('/v1/device')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('GET /v1/device/host-config', () => {
		// Stub external dependencies
		let getHostConfigStub: SinonStub;
		beforeEach(() => {
			getHostConfigStub = stub(hostConfig, 'get');
		});
		afterEach(() => {
			getHostConfigStub.restore();
		});

		it('responds with 200 and host config', async () => {
			getHostConfigStub.resolves({ network: { hostname: 'deadbeef' } });
			await request(api)
				.get('/v1/device/host-config')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200, { network: { hostname: 'deadbeef' } });
		});

		it('responds with 503 for other errors that occur during request', async () => {
			getHostConfigStub.throws(new Error());
			await request(api)
				.get('/v1/device/host-config')
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});

	describe('PATCH /v1/device/host-config', () => {
		before(() => stub(actions, 'patchHostConfig'));
		after(() => (actions.patchHostConfig as SinonStub).restore());

		it('warns on console when sent a malformed patch body', async () => {
			await request(api)
				.patch('/v1/device/host-config')
				.send({})
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200)
				.then(() => {
					expect(log.warn as SinonStub).to.have.been.calledWith(
						"Key 'network' must exist in PATCH body",
					);
				});
		});

		it('responds with 200 if patch successful', async () => {
			(actions.patchHostConfig as SinonStub).resolves();
			await request(api)
				.patch('/v1/device/host-config')
				.send({ network: { hostname: 'deadbeef' } })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(200);
		});

		it('responds with 423 for update lock errors', async () => {
			(actions.patchHostConfig as SinonStub).throws(new UpdatesLockedError());
			await request(api)
				.patch('/v1/device/host-config')
				.send({ network: { hostname: 'deadbeef' } })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(423);
		});

		it('responds with 400 for BadRequestErrors', async () => {
			(actions.patchHostConfig as SinonStub).throws(new BadRequestError());
			await request(api)
				.patch('/v1/device/host-config')
				.send({ network: { hostname: 'deadbeef' } })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(400);
		});

		it('responds with 503 for other errors that occur during patch', async () => {
			(actions.patchHostConfig as SinonStub).throws(new Error());
			await request(api)
				.patch('/v1/device/host-config')
				.send({ network: { hostname: 'deadbeef' } })
				.set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`)
				.expect(503);
		});
	});
});
