import { expect } from 'chai';

import * as sinon from 'sinon';
import * as volumeManager from '~/src/compose/volume-manager';
import { Volume } from '~/src/compose/volume';
import { createDockerImage } from '~/test-lib/docker-helper';

import Docker from 'dockerode';

describe('compose/volume-manager', () => {
	const docker = new Docker();
	after(async () => {
		await docker.pruneContainers();
		await docker.pruneVolumes();
		await docker.pruneImages({ filters: { dangling: { false: true } } });
	});

	describe('Retrieving volumes from the engine', () => {
		it('gets all supervised Volumes', async () => {
			// Setup volume data
			await Promise.all([
				docker.createVolume({
					Name: Volume.generateDockerName(1, 'redis'),
					// Recently created volumes contain io.balena.supervised label
					Labels: { 'io.balena.supervised': '1' },
				}),
				docker.createVolume({
					Name: Volume.generateDockerName(1, 'mysql'),
					// Recently created volumes contain io.balena.supervised label and app-uuid
					Labels: {
						'io.balena.supervised': '1',
						'io.balena.app-uuid': 'deadbeef',
					},
				}),
				docker.createVolume({
					Name: Volume.generateDockerName(2, 'backend'),
					// Old Volumes will not have labels
				}),
				// Volume not created by the Supervisor
				docker.createVolume({ Name: 'user_created_volume' }),
				docker.createVolume({
					Name: 'decoy',
					// Added decoy to really test the inference (should not return)
					Labels: { 'io.balena.supervised': '1' },
				}),
			]);

			// Perform test
			await expect(volumeManager.getAll()).to.eventually.have.deep.members([
				{
					appId: 1,
					appUuid: undefined,
					config: {
						driver: 'local',
						driverOpts: {},
						labels: {
							'io.balena.supervised': '1',
						},
					},
					name: 'redis',
				},
				{
					appId: 1,
					appUuid: 'deadbeef',
					config: {
						driver: 'local',
						driverOpts: {},
						labels: {
							'io.balena.supervised': '1',
							'io.balena.app-uuid': 'deadbeef',
						},
					},
					name: 'mysql',
				},
				{
					appId: 2,
					appUuid: undefined,
					config: {
						driver: 'local',
						driverOpts: {},
						labels: {},
					},
					name: 'backend',
				},
			]);

			// Cleanup volumes
			await Promise.all([
				docker.getVolume(Volume.generateDockerName(1, 'redis')).remove(),
				docker.getVolume(Volume.generateDockerName(1, 'mysql')).remove(),
				docker.getVolume(Volume.generateDockerName(2, 'backend')).remove(),
				docker.getVolume('user_created_volume').remove(),
				docker.getVolume('decoy').remove(),
			]);
		});

		it('can parse null Volumes', async () => {
			// Perform test with no volumes
			await expect(volumeManager.getAll()).to.eventually.deep.equal([]);
		});

		it('gets the volume for specific application', async () => {
			// Setup volume data
			await Promise.all([
				docker.createVolume({
					Name: Volume.generateDockerName(111, 'app'),
					Labels: {
						'io.balena.supervised': '1',
					},
				}),
				docker.createVolume({
					Name: Volume.generateDockerName(222, 'otherApp'),
					Labels: {
						'io.balena.supervised': '1',
					},
				}),
			]);

			// Perform test
			await expect(volumeManager.getAllByAppId(111)).to.eventually.deep.equal([
				{
					appId: 111,
					appUuid: undefined,
					config: {
						driver: 'local',
						driverOpts: {},
						labels: {
							'io.balena.supervised': '1',
						},
					},
					name: 'app',
				},
			]);

			// Cleanup volumes
			await Promise.all([
				docker.getVolume(Volume.generateDockerName(111, 'app')).remove(),
				docker.getVolume(Volume.generateDockerName(222, 'otherApp')).remove(),
			]);
		});
	});

	describe('Creating volumes', () => {
		it('creates a volume if it does not exist', async () => {
			// The volume does not exist on the engine before
			await expect(
				docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
			).to.be.rejected;

			// Volume to create
			const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});

			// Create volume
			await volumeManager.create(volume);

			// Check the volume should have been created
			await expect(
				docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
			).to.not.be.rejected;

			// Cleanup volumes
			await Promise.all([
				docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
			]);
		});

		it('does not try to create a volume that already exists', async () => {
			// Setup volume data
			await docker.createVolume({
				Name: Volume.generateDockerName(111, 'main'),
				Labels: {
					'io.balena.supervised': '1',
				},
			});

			// Create compose object for volume already set up in mock engine
			const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
			sinon.spy(volume, 'create');

			// Create volume
			await volumeManager.create(volume);

			// Check volume was not created
			expect(volume.create).to.not.have.been.called;

			// Cleanup volumes
			await Promise.all([
				docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
			]);
		});
	});

	describe('Removing volumes', () => {
		it('removes a volume if it exists', async () => {
			// Setup volume data
			await Promise.all([
				docker.createVolume({
					Name: Volume.generateDockerName(111, 'main'),
					Labels: {
						'io.balena.supervised': '1',
					},
				}),
			]);

			// Volume to remove
			const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});

			// Remove volume
			await volumeManager.remove(volume);

			// Check volume was removed
			await expect(
				docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
			).to.be.rejected;
		});

		it('does nothing on removal if the volume does not exist', async () => {
			// Setup volume data
			await Promise.all([
				docker.createVolume({
					Name: 'decoy-volume',
				}),
			]);

			// Volume to remove
			const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});

			// Remove volume
			await expect(volumeManager.remove(volume)).to.not.be.rejected;

			// Cleanup volumes
			await Promise.all([docker.getVolume('decoy-volume').remove()]);
		});
	});

	describe('Removing orphaned volumes', () => {
		it('removes any remaining unreferenced volumes after services have been deleted', async () => {
			// Setup volume data
			await Promise.all([
				docker.createVolume({
					Name: 'some-volume',
				}),
				// This volume is still referenced in the target state
				docker.createVolume({
					Name: Volume.generateDockerName(111, 'main'),
					Labels: {
						'io.balena.supervised': '1',
					},
				}),
				docker.createVolume({
					Name: Volume.generateDockerName(222, 'old'),
					Labels: {
						'io.balena.supervised': '1',
					},
				}),
				// This volume is referenced by a container
				docker.createVolume({
					Name: 'other-volume',
				}),
			]);

			// Create an empty image
			await createDockerImage('hello', ['io.balena.testing=1'], docker);

			// Create a container from the image
			const { id: containerId } = await docker.createContainer({
				Image: 'hello',
				Cmd: ['true'],
				HostConfig: {
					Binds: ['other-volume:/data'],
				},
			});

			await expect(
				volumeManager.removeOrphanedVolumes([
					// Keep any volumes in the target state
					Volume.generateDockerName(111, 'main'),
				]),
			).to.not.be.rejected;

			// All volumes should have been deleted
			expect(await docker.listVolumes())
				.to.have.property('Volumes')
				.that.has.lengthOf(2);

			// Reference volume should have been kept
			await expect(
				docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
			).to.not.be.rejected;
			await expect(docker.getVolume('other-volume').inspect()).to.not.be
				.rejected;

			// Cleanup
			await Promise.all([
				docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
				docker.getContainer(containerId).remove(),
			]);

			await Promise.all([
				docker.getImage('hello').remove(),
				docker.getVolume('other-volume').remove(),
			]);
		});
	});
});
