Promise = require 'bluebird'
_ = require 'lodash'
m = require 'mochainon'
{ stub } = m.sinon
m.chai.use(require('chai-events'))
{ expect } = m.chai

prepare = require './lib/prepare'
DeviceState = require '../src/device-state'
{ DB } = require('../src/db')
{ Config } = require('../src/config')
{ RPiConfigBackend } = require('../src/config/backend')

{ Service } = require '../src/compose/service'

mockedInitialConfig = {
	'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
	'RESIN_SUPERVISOR_DELTA': 'false'
	'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
	'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
	'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
	'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
	'RESIN_SUPERVISOR_DELTA_VERSION': '2'
	'RESIN_SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
	'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
	'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
	'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
	'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
	'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
}

testTarget1 = {
	local: {
		name: 'aDevice'
		config: {
			'HOST_CONFIG_gpu_mem': '256'
			'SUPERVISOR_CONNECTIVITY_CHECK': 'true'
			'SUPERVISOR_DELTA': 'false'
			'SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
			'SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
			'SUPERVISOR_DELTA_RETRY_COUNT': '30'
			'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
			'SUPERVISOR_DELTA_VERSION': '2'
			'SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
			'SUPERVISOR_LOCAL_MODE': 'false'
			'SUPERVISOR_LOG_CONTROL': 'true'
			'SUPERVISOR_OVERRIDE_LOCK': 'false'
			'SUPERVISOR_POLL_INTERVAL': '60000'
			'SUPERVISOR_VPN_CONTROL': 'true'
			'SUPERVISOR_PERSISTENT_LOGGING': 'false'
		}
		apps: {
			'1234': {
				appId: 1234
				name: 'superapp'
				commit: 'abcdef'
				releaseId: 1
				services: [
					{
						appId: 1234
						serviceId: 23
						imageId: 12345
						serviceName: 'someservice'
						releaseId: 1
						image: 'registry2.resin.io/superapp/abcdef:latest'
						labels: {
							'io.resin.something': 'bar'
						}
					}
				]
				volumes: {}
				networks: {}
			}
		}
	}
	dependent: { apps: [], devices: [] }
}

testTarget2 = {
	local: {
		name: 'aDeviceWithDifferentName'
		config: {
			'RESIN_HOST_CONFIG_gpu_mem': '512'
		}
		apps: {
			'1234': {
				name: 'superapp'
				commit: 'afafafa'
				releaseId: 2
				services: {
					'23': {
						serviceName: 'aservice'
						imageId: 12345
						image: 'registry2.resin.io/superapp/edfabc'
						environment: {
							'FOO': 'bar'
						}
						labels: {}
					},
					'24': {
						serviceName: 'anotherService'
						imageId: 12346
						image: 'registry2.resin.io/superapp/afaff'
						environment: {
							'FOO': 'bro'
						}
						labels: {}
					}
				}
			}
		}
	}
	dependent: { apps: [], devices: [] }
}
testTargetWithDefaults2 = {
	local: {
		name: 'aDeviceWithDifferentName'
		config: {
			'HOST_CONFIG_gpu_mem': '512'
			'SUPERVISOR_CONNECTIVITY_CHECK': 'true'
			'SUPERVISOR_DELTA': 'false'
			'SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
			'SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
			'SUPERVISOR_DELTA_RETRY_COUNT': '30'
			'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
			'SUPERVISOR_DELTA_VERSION': '2'
			'SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
			'SUPERVISOR_LOCAL_MODE': 'false'
			'SUPERVISOR_LOG_CONTROL': 'true'
			'SUPERVISOR_OVERRIDE_LOCK': 'false'
			'SUPERVISOR_POLL_INTERVAL': '60000'
			'SUPERVISOR_VPN_CONTROL': 'true'
			'SUPERVISOR_PERSISTENT_LOGGING': 'false'
		}
		apps: {
			'1234': {
				appId: 1234
				name: 'superapp'
				commit: 'afafafa'
				releaseId: 2
				services: [
					_.merge({ appId: 1234, serviceId: 23, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['23'])),
					_.merge({ appId: 1234, serviceId: 24, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['24']))
				]
				volumes: {}
				networks: {}
			}
		}
	}
	dependent: { apps: [], devices: [] }
}

testTargetInvalid = {
	local: {
		name: 'aDeviceWithDifferentName'
		config: {
			'RESIN_HOST_CONFIG_gpu_mem': '512'
		}
		apps: [
			{
				appId: '1234'
				name: 'superapp'
				commit: 'afafafa'
				releaseId: '2'
				config: {}
				services: [
					{
						serviceId: '23'
						serviceName: 'aservice'
						imageId: '12345'
						image: 'registry2.resin.io/superapp/edfabc'
						config: {}
						environment: {
							' FOO': 'bar'
						}
						labels: {}
					},
					{
						serviceId: '24'
						serviceName: 'anotherService'
						imageId: '12346'
						image: 'registry2.resin.io/superapp/afaff'
						config: {}
						environment: {
							'FOO': 'bro'
						}
						labels: {}
					}
				]
			}
		]
	}
	dependent: { apps: [], devices: [] }
}

describe 'deviceState', ->
	before ->
		prepare()
		@db = new DB()
		@config = new Config({ @db })
		@logger = {
			clearOutOfDateDBLogs: ->
		}
		eventTracker = {
			track: console.log
		}
		stub(Service, 'extendEnvVars').callsFake (env) ->
			env['ADDITIONAL_ENV_VAR'] = 'foo'
			return env
		@deviceState = new DeviceState({ @db, @config, eventTracker, @logger })
		stub(@deviceState.applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
		stub(@deviceState.applications.images, 'inspectByName').callsFake ->
			Promise.try ->
				err = new Error()
				err.statusCode = 404
				throw err
		@deviceState.deviceConfig.configBackend = new RPiConfigBackend()
		@db.init()
		.then =>
			@config.init()

	after ->
		Service.extendEnvVars.restore()
		@deviceState.applications.docker.getNetworkGateway.restore()
		@deviceState.applications.images.inspectByName.restore()

	it 'loads a target state from an apps.json file and saves it as target state, then returns it', ->
		stub(@deviceState.applications.images, 'save').returns(Promise.resolve())
		stub(@deviceState.deviceConfig, 'getCurrent').returns(Promise.resolve(mockedInitialConfig))
		@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json')
		.then =>
			@deviceState.getTarget()
		.then (targetState) ->
			testTarget = _.cloneDeep(testTarget1)
			testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) ->
				s.imageName = s.image
				return Service.fromComposeObject(s, { appName: 'superapp' })
			# We serialize and parse JSON to avoid checking fields that are functions or undefined
			expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
		.finally =>
			@deviceState.applications.images.save.restore()
			@deviceState.deviceConfig.getCurrent.restore()

	it 'stores info for pinning a device after loading an apps.json with a pinDevice field', ->
		stub(@deviceState.applications.images, 'save').returns(Promise.resolve())
		stub(@deviceState.deviceConfig, 'getCurrent').returns(Promise.resolve(mockedInitialConfig))
		@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json')
		.then =>
			@deviceState.applications.images.save.restore()
			@deviceState.deviceConfig.getCurrent.restore()

			@config.get('pinDevice').then (pinned) ->
				expect(pinned).to.have.property('app').that.equals(1234)
				expect(pinned).to.have.property('commit').that.equals('abcdef')

	it 'emits a change event when a new state is reported', ->
		@deviceState.reportCurrentState({ someStateDiff: 'someValue' })
		expect(@deviceState).to.emit('change')

	it 'returns the current state'

	it 'writes the target state to the db with some extra defaults', ->
		testTarget = _.cloneDeep(testTargetWithDefaults2)
		Promise.map testTarget.local.apps['1234'].services, (s) =>
			@deviceState.applications.images.normalise(s.image)
			.then (imageName) ->
				s.image = imageName
				s.imageName = imageName
				Service.fromComposeObject(s, { appName: 'supertest' })
		.then (services) =>
			testTarget.local.apps['1234'].services = services
			@deviceState.setTarget(testTarget2)
		.then =>
			@deviceState.getTarget()
		.then (target) ->
			expect(JSON.parse(JSON.stringify(target))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))

	it 'does not allow setting an invalid target state', ->
		promise = @deviceState.setTarget(testTargetInvalid)
		promise.catch(->)
		expect(promise).to.be.rejected

	it 'allows triggering applying the target state', (done) ->
		stub(@deviceState, 'applyTarget').returns(Promise.resolve())
		@deviceState.triggerApplyTarget({ force: true })
		expect(@deviceState.applyTarget).to.not.be.called
		setTimeout =>
			expect(@deviceState.applyTarget).to.be.calledWith({ force: true, initial: false })
			@deviceState.applyTarget.restore()
			done()
		, 5

	it 'applies the target state for device config'

	it 'applies the target state for applications'