import cluster from 'cluster'
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Core from '../core/core.mjs'
import Util from '../core/util.mjs'
import { createFakeLog } from './helpers.mjs'
import StaticProvider from '../core/providers/static.mjs'
import lowdb from '../core/db.mjs'

const util = new Util(import.meta.url)
const log = createFakeLog()
let db

t.before(function() {
  return lowdb({}, log, null).then(function(res) {
    db = res
  })
})

t.describe('Core.addProvider()', function() {
  t.beforeEach(function() {
    Core.providers.clear()
  })

  t.after(function() {
    Core.providers.clear()
  })

  t.test('should fail if name is not a string', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        Core.addProvider(check[0], StaticProvider)
      }, function(err) {
        assert.match(err.message, /name/i)
        assert.match(err.message, /string/i)
        return true
      }, `throw if name is ${check[1]}`)
    })
  })
  
  t.test('should fail if provider not a function', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        Core.addProvider('insertname', check[0])
      }, function(err) {
        assert.match(err.message, /provider/i)
        assert.match(err.message, /class/i)
        assert.match(err.message, /insertname/i)
        return true
      }, `throw if provider is ${check[1]}`)
    })
  })
  
  t.test('should fail if provider instance is missing checkConfig', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        let provider = function() { this.getLatestVersion = function() {}; this.checkConfig = check[0] }
        Core.addProvider('somename', provider)
      }, function(err) {
        assert.match(err.message, /provider/i)
        assert.match(err.message, /class/i)
        assert.match(err.message, /missing/i)
        assert.match(err.message, /checkConfig/i)
        assert.match(err.message, /somename/i)
        return true
      }, `throw if provider checkConfig is ${check[1]}`)
    })
  })
  
  t.test('should fail if provider instance is missing getLatestVersion', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        let provider = function() { this.checkConfig = function() {}; this.getLatestVersion = check[0] }
        Core.addProvider('somename', provider)
      }, function(err) {
        assert.match(err.message, /provider/i)
        assert.match(err.message, /class/i)
        assert.match(err.message, /missing/i)
        assert.match(err.message, /getLatestVersion/i)
        assert.match(err.message, /somename/i)
        return true
      }, `throw if provider getLatestVersion is ${check[1]}`)
    })
  })

  t.test('should otherwise add provider to map', function() {
    assert.strictEqual(Core.providers.size, 0)
    Core.addProvider('testnamehere', StaticProvider)
    assert.strictEqual(Core.providers.size, 1)
    assert.strictEqual(Core.providers.get('testnamehere'), StaticProvider)
  })
})

t.describe('#constructor()', function() {
  t.test('should throw if util is not util', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
      [Util, 'not instance'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        new Core(db, check[0], log)
      }, function(err) {
        assert.match(err.message, /util/i)
        assert.match(err.message, /instance/i)
        return true
      }, `throw if util is ${check[1]}`)
    })
  })
  
  t.test('should throw if db is not lowdb', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
      [lowdb.Low, 'not instance'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        new Core(check[0], util, log)
      }, function(err) {
        assert.match(err.message, /db/i)
        assert.match(err.message, /instance/i)
        return true
      }, `throw if db is ${check[1]}`)
    })
  })
  
  t.test('should throw if log is not an object with event', function() {
    let func = function() {}
    let validEvent = { info: func, warn: func, error: func }
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      [null, 'null'],
      [undefined, 'undefined'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
      [{warn: func, event: validEvent }, 'log only warn'],
      [{error: func, event: validEvent }, 'log only error'],
      [{info: func, event: validEvent }, 'log only info'],
      [{warn: func, error: func, event: validEvent }, 'log only warn and error'],
      [{warn: func, info: func, event: validEvent }, 'log only warn and info'],
      [{error: func, info: func, event: validEvent }, 'log only error and info'],
      [{ warn: func, error: func, info: func, event: { warn: func } }, 'event only warn'],
      [{ warn: func, error: func, info: func, event: { error: func } }, 'event only error'],
      [{ warn: func, error: func, info: func, event: { info: func } }, 'event only info'],
      [{ warn: func, error: func, info: func, event: { warn: func, error: func } }, 'event only warn and error'],
      [{ warn: func, error: func, info: func, event: { warn: func, info: func } }, 'event only warn and info'],
      [{ warn: func, error: func, info: func, event: { error: func, info: func } }, 'event only error and info'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        new Core(db, util, check[0], func)
      }, function(err) {
        assert.match(err.message, /log/i)
        assert.match(err.message, /valid/i)
        return true
      }, `throw if log is ${check[1]}`)
    })
  })

  t.test('should throw if restart is not a function', function() {
    let tests = [
      [1, 'number'],
      [0, 'false number'],
      ['asdf', 'string'],
      ['', 'false string'],
      [[], 'array'],
      [{}, 'object'],
    ]

    tests.forEach(function(check) {
      assert.throws(function() {
        new Core(db, util, log, check[0])
      }, function(err) {
        assert.match(err.message, /restart/i)
        assert.match(err.message, /function/i)
        return true
      }, `throw if restart is ${check[1]}`)
    })
  })

  t.test('should accept log, util and close function', function() {
    const assertLog = log
    const assertRestarter = function() { }

    let core = new Core(db, util, assertLog, assertRestarter)
    assert.strictEqual(core.db, db)
    assert.strictEqual(core.util, util)
    assert.strictEqual(core.log, assertLog)
    assert.strictEqual(core.restart, assertRestarter)
    assert.deepStrictEqual(core.applications, [])
    assert.ok(core.applicationMap)
    assert.strictEqual(core.isSlave, false)
  })
})

t.describe('#getApplication()', function() {

  t.test('should return application based on the name', function() {
    const assertName = 'Yami no Naka'
    const assertApplication = { a: 1 }
    let core = new Core(db, util, log)
    core.applicationMap.set(assertName, assertApplication)
    assert.strictEqual(core.getApplication(assertName), assertApplication)
  })
})

t.describe('#init()', function() {
  const assertProviderName = 'Kyousuu Gakku Gogyou Kikan'
  let core
  let fakeUtil
  let fakeProvider
  let fakeProviderConfig

  function FakeProvider(config) {
    fakeProvider(config)
    this.static = true
    this.checkConfig = fakeProviderConfig
  }

  t.beforeEach(function() {
    log.error.reset()
    core = new Core(db, util, log)
    core.util = fakeUtil = {
      verifyConfig: stub(),
      getAppNames: stub().returns([]),
    }
    fakeProvider = stub()
    fakeProviderConfig = stub()
    Core.providers.set(assertProviderName, FakeProvider)
  })

  t.after(function() {
    return Promise.all([
      fs.rm('./log_test_1.log', { recursive: true, force: true }),
    ])
  })

  t.test('it should call util.verifyConfig correctly', async function() {
    const assertError = new Error('Red faction IO drive mix')
    const assertConfig = { a: 1 }
    db.config = assertConfig
    fakeUtil.verifyConfig.throws(assertError)

    let err = await assert.isRejected(core.init())
    assert.strictEqual(err, assertError)
    assert.strictEqual(fakeUtil.verifyConfig.firstCall[0], assertConfig)
  })

  t.test('should call util.getNames correctly', async function() {
    const assertError = new Error('Hero within')
    const assertConfig = { a: 1 }
    db.config = assertConfig
    fakeUtil.getAppNames.throws(assertError)

    let err = await assert.isRejected(core.init())
    assert.strictEqual(err, assertError)
    assert.strictEqual(fakeUtil.getAppNames.firstCall[0], assertConfig)
  })

  t.test('should call provider constructor correctly', async function() {
    const assertError = new Error('Funny days')
    const assertAppName = 'Tsuugakuro'
    const assertConfig = {
      [assertAppName]: {
        provider: assertProviderName,
      }
    }
    db.config = assertConfig
    fakeProvider.throws(assertError)
    fakeUtil.getAppNames.returns([assertAppName])
    
    let err = await assert.isRejected(core.init())
    assert.notStrictEqual(err, assertError)
    assert.strictEqual(log.error.firstCall[0], assertError)
    assert.match(err.message, /successful/i)
    assert.match(err.message, /none/i)
    assert.strictEqual(fakeProvider.firstCall[0], assertConfig[assertAppName])
  })

  t.test('should call provider checkConfig correctly', async function() {
    const assertAppName = 'Zetsubou'
    const assertConfig = {
      [assertAppName]: {
        provider: assertProviderName,
      }
    }
    db.config = assertConfig

    const assertError = new Error('Shousou')
    fakeProviderConfig.rejects(assertError)
    fakeUtil.getAppNames.returns([assertAppName])
    
    let err = await assert.isRejected(core.init())
    assert.strictEqual(log.error.firstCall[0], assertError)
    assert.match(err.message, /successful/i)
    assert.match(err.message, /none/i)
    assert.strictEqual(fakeProviderConfig.firstCall[0], assertConfig[assertAppName])
  })

  t.test('should create an application with the provider and name and config', async function() {
    const assertAppName = 'Yasashii Ketsumatsu'
    const assertTestString = 'Serozore no Omoi'
    const assertConfig = {
      [assertAppName]: {
        provider: assertProviderName,
        teststring: assertTestString,
      }
    }
    db.config = assertConfig
    fakeUtil.getAppNames.returns([assertAppName])
    assert.strictEqual(core.applications.length, 0)

    await core.init()
    let application = core.getApplication(assertAppName)
    
    assert.ok(application)
    assert.strictEqual(core.applications.length, 1)
    assert.strictEqual(core.applications[0], application)
    assert.strictEqual(application.name, assertAppName)
    assert.strictEqual(application.ctx.db, core.db)
    assert.strictEqual(application.ctx.util, core.util)
    assert.notStrictEqual(application.ctx.log, core.log)
    assert.strictEqual(application.ctx.core, core)
    assert.strictEqual(application.config.teststring, assertTestString)
    assert.ok(application.fresh)
    assert.ok(application.provider instanceof FakeProvider)
  })

  t.test('should continue even if one application fails', async function() {
    log.error.reset()
    const assertProviderFailName = 'Knee Socks'
    let stubFailCheckConfig = stub()
    function FakeFailProvider(config) {
      fakeProvider(config)
      this.static = true
      this.checkConfig = stubFailCheckConfig
    }
    Core.providers.set(assertProviderFailName, FakeFailProvider)

    const assertError = new Error('Justice of light')
    const assertPathLog = 'log_test_1.log'
    const assertFirstAppName = 'Kaeshite'
    const assertSecondAppName = 'Motteke'
    const assertTestFirstString = 'Magia'
    const assertTestSecondString = 'Snow Falling'
    const assertConfig = {
      [assertFirstAppName]: {
        provider: assertProviderName,
        teststring: assertTestFirstString,
        log: [
          { path: assertPathLog, level: 'info' },
          { stream: 'process.stdout', level: 'warn' },
        ]
      },
      [assertSecondAppName]: {
        provider: assertProviderFailName,
        teststring: assertTestSecondString,
      },
    }
    stubFailCheckConfig.throws(assertError)
    db.config = assertConfig
    fakeUtil.getAppNames.returns([assertSecondAppName, assertFirstAppName])
    assert.strictEqual(core.applications.length, 0)

    await core.init()
    let application = core.getApplication(assertFirstAppName)
    
    assert.ok(application)
    assert.strictEqual(core.applications.length, 1)
    assert.strictEqual(core.applications[0], application)
    assert.strictEqual(application.name, assertFirstAppName)
    assert.strictEqual(application.ctx.db, core.db)
    assert.strictEqual(application.ctx.util, core.util)
    assert.notStrictEqual(application.ctx.log, core.log) // 
    assert.strictEqual(application.ctx.core, core)
    assert.strictEqual(application.config.teststring, assertTestFirstString)
    assert.ok(application.fresh)
    assert.ok(application.provider instanceof FakeProvider)
    assert.ok(log.error.called)
    assert.strictEqual(log.error.firstCall[0], assertError)
    assert.match(log.error.firstCall[1], new RegExp(assertProviderFailName))
    assert.match(log.error.firstCall[1], new RegExp(assertSecondAppName))
    assert.match(log.error.firstCall[1], new RegExp(assertError.message))
    assert.ok(application.ctx.log.streams.length >= 4)
    assert.strictEqual(application.ctx.log.streams[0].path, assertPathLog)
    assert.strictEqual(application.ctx.log.streams[0].level, 30)
    assert.strictEqual(application.ctx.log.streams[0].type, 'file')
    assert.strictEqual(application.ctx.log.streams[1].stream, process.stdout)
    assert.strictEqual(application.ctx.log.streams[1].level, 40)
    assert.strictEqual(application.ctx.log.streams[1].type, 'stream')
  })

  t.test('should listen on cluster messages if one or more are cluster on', async function() {
    const assertAppName1 = 'Dai Sagara Yoshiharu'
    const assertAppName2 = 'Kuryo'
    const assertCoreName = 'Ichuu'
    const assertPayload1 = { a: 1 }
    const assertPayload2 = { b: 2 }
    const assertConfig = {
      name: assertCoreName,
      [assertAppName1]: {
        provider: assertProviderName,
        cluster: 2,
      },
      [assertAppName2]: {
        provider: assertProviderName,
        cluster: 1,
      },
    }
    db.config = assertConfig
    fakeUtil.getAppNames.returns([assertAppName1, assertAppName2])
    assert.strictEqual(core.applications.length, 0)

    await core.init()

    core.log.emit = stub()

    assert.strictEqual(core.applications.length, 2)

    let app1 = core.getApplication(assertAppName1)
    let app2 = core.getApplication(assertAppName2)

    app1.ctx.log.emit = stub()
    app2.ctx.log.emit = stub()

    cluster.emit('message', null, {
      apptarget: app1.name,
      type: 'newlog',
      payload: assertPayload1
    })

    assert.notOk(core.log.emit.called)
    assert.ok(app1.ctx.log.emit.called)
    assert.ok(app1.ctx.log.emit.firstCall[0], 'newlog')
    assert.ok(app1.ctx.log.emit.firstCall[1], assertPayload1)
    assert.notOk(app2.ctx.log.emit.called)

    app1.ctx.log.emit.reset()
    
    cluster.emit('message', null, {
      apptarget: app2.name,
      type: 'newlog',
      payload: assertPayload2
    })

    assert.notOk(core.log.emit.called)
    assert.notOk(app1.ctx.log.emit.called)
    assert.ok(app2.ctx.log.emit.called)
    assert.ok(app2.ctx.log.emit.firstCall[0], 'newlog')
    assert.ok(app2.ctx.log.emit.firstCall[1], assertPayload2)

    app2.ctx.log.emit.reset()

    let tests = [
      null,
      undefined,
      12412,
      'asdfag',
      {},
      { apptarget: 12421, type: 'newlog', payload: {}},
      { apptarget: {}, type: 'newlog', payload: {}},
      { apptarget: null, type: 'newlog', payload: {}},
      { type: 'newlog', payload: {}},
      { apptarget: app1.name, type: 12421, payload: {}},
      { apptarget: app1.name, type: {}, payload: {}},
      { apptarget: app1.name, type: null, payload: {}},
      { apptarget: app1.name, payload: {}},
      { apptarget: app1.name, type: 'newlog', payload: 12421},
      { apptarget: app1.name, type: 'newlog', payload: null},
      { apptarget: app1.name, type: 'newlog', payload: 'test'},
    ]

    tests.forEach(function(test) {
      cluster.emit('message', null, test)
      
      assert.notOk(core.log.emit.called)
      assert.notOk(app1.ctx.log.emit.called)
      assert.notOk(app2.ctx.log.emit.called)
    })

    cluster.emit('message', null, {
      apptarget: assertCoreName,
      type: 'newlog',
      payload: assertPayload1
    })
    
    assert.notOk(app1.ctx.log.emit.called)
    assert.notOk(app2.ctx.log.emit.called)
    assert.ok(core.log.emit.called)
    assert.ok(core.log.emit.called)
    assert.ok(core.log.emit.firstCall[0], 'newlog')
    assert.ok(core.log.emit.firstCall[1], assertPayload1)
  })
})

t.describe('#run()', function() {
  let core
  let testAppOneName
  let testAppTwoName
  let stubRunApplication
  let stubOnOrOnce
  let stubWrite

  t.beforeEach(function() {
    testAppOneName = 'Tenshi'
    testAppTwoName = 'no CLOVER'
    db.data.core = {
      [testAppOneName]: {
        versions: []
      },
      [testAppTwoName]: {
        versions: []
      },
    }
    core = new Core(db, util, log)
    core.runApplication = stubRunApplication = stub().resolves()
    db.write = stubWrite = stub().resolves()
    log.info.reset()
    log.warn.reset()
    log.error.reset()
    for (let name of [testAppOneName, testAppTwoName]) {
      let onOrOnce = stub()
      let app = {
        name: name,
        fresh: false,
        on: onOrOnce,
        once: onOrOnce,
        ctx: {
          log: {
            info: stub(),
            warn: stub(),
            error: stub()
          },
        },
        update: stub().resolves(),
        startAutoupdater: stub(),
      }
      core.applicationMap.set(name, app)
      core.applications.push(app)
    }
  })

  t.test('should call update on all applications', async function() {
    const assertFirstError = new Error('Manatsu')
    const assertSecondError = new Error('no Photograph')
    core.applicationMap.get(testAppOneName).update.rejects(assertFirstError)
    core.applicationMap.get(testAppTwoName).update.rejects(assertSecondError)

    await core.run()
    
    assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], /updat/)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message))
    assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], /updat/)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message))
  })
  

  t.test('app.on updated should be hooked eventually and should call runApplication', async function() {
    assert.notOk(core.applicationMap.get(testAppOneName).once.called)
    assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
    assert.notOk(core.applicationMap.get(testAppOneName).on.called)
    assert.notOk(core.applicationMap.get(testAppTwoName).on.called)
    const assertFirstError = new Error('Manatsu')
    const assertSecondError = new Error('no Photograph')
    core.applicationMap.get(testAppOneName).update.returnWith(function() {
      assert.notOk(core.applicationMap.get(testAppOneName).once.called)
      assert.notOk(core.applicationMap.get(testAppOneName).on.called)
      return Promise.reject(assertFirstError)
    })
    core.applicationMap.get(testAppTwoName).update.returnWith(function() {
      assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
      assert.notOk(core.applicationMap.get(testAppOneName).on.called)
      return Promise.reject(assertSecondError)
    })
    core.runApplication.returnWith(function(app) {
      assert.notOk(app.once.called)
      assert.notOk(app.on.called)
      return Promise.resolve()
    })

    await core.run()

    assert.ok(core.applicationMap.get(testAppOneName).on.called)
    assert.ok(core.applicationMap.get(testAppTwoName).on.called)
    assert.strictEqual(core.applicationMap.get(testAppOneName).on.firstCall[0], 'updated')
    assert.strictEqual(core.applicationMap.get(testAppTwoName).on.firstCall[0], 'updated')
    
    assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], /updat/)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message))
    assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], /updat/)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message))

   assert.strictEqual(stubRunApplication.callCount, 2)

   assert.strictEqual(stubRunApplication.getCallN(1)[0], core.applications[0])
   assert.strictEqual(stubRunApplication.getCallN(2)[0], core.applications[1])

   core.runApplication.resolves()

   core.applications[0].on.firstCall[1]()
   assert.strictEqual(stubRunApplication.callCount, 3)
   assert.strictEqual(stubRunApplication.getCallN(3)[0], core.applications[0])
   
   core.applications[0].on.firstCall[1]()
   assert.strictEqual(stubRunApplication.callCount, 4)
   assert.strictEqual(stubRunApplication.getCallN(4)[0], core.applications[0])

   
   core.applications[1].on.firstCall[1]()
   assert.strictEqual(stubRunApplication.callCount, 5)
   assert.strictEqual(stubRunApplication.getCallN(5)[0], core.applications[1])
  })

  t.test('should call startAutoupdater on all applications', async function() {
    stubRunApplication.rejects(new Error('not seen'))
    assert.notOk(core.applications[0].startAutoupdater.called)
    assert.notOk(core.applications[1].startAutoupdater.called)

    await assert.isRejected(core.run())
    
    assert.ok(core.applications[0].startAutoupdater.called)
    assert.ok(core.applications[1].startAutoupdater.called)
  })

  t.test('should be safe to call multiple times', async function() {
    await core.run()

    assert.strictEqual(stubRunApplication.callCount, 2)

    await core.run()
    await core.run()
    await core.run()
    await core.run()

    assert.strictEqual(stubRunApplication.callCount, 2)
  })

  t.test('should call runApplication on all applications', async function() {
    const assertFirstError = new Error('Manatsu')
    const assertSecondError = new Error('no Photograph')
    const assertNoError = new Error('unseen')
    stubRunApplication.onCallN(1).rejects(assertFirstError)
    stubRunApplication.onCallN(2).rejects(assertSecondError)
    stubRunApplication.onCallN(3).rejects(assertNoError)

    await assert.isRejected(core.run())
    
    assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], /run/)
    assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message))
    assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], /run/)
    assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message))
  })
  
  t.test('should throw if all applications fail', async function() {
    stubRunApplication.rejects(new Error('bla'))

    let err = await assert.isRejected(core.run())
    
    assert.match(err.message, /no/i)
    assert.match(err.message, /application/i)
  })
  
  t.test('should not throw if at least one application is run', async function() {
    stubRunApplication.onCallN(1).rejects(new Error('Scramble'))

    await core.run()
    
    assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1)
    assert.strictEqual(core.applications[1].ctx.log.error.callCount, 0)
  })
  
  t.test('should not throw if all applications are running', async function() {
    await core.run()
    
    assert.strictEqual(log.error.callCount, 0)
  })
})

t.describe('#runApplication()', function() {
  let core
  let testApp
  let testAppName
  let stubWrite

  t.beforeEach(function() {
    testAppName = 'Precure'
    db.data.core = {
      [testAppName]: {
        versions: []
      }
    }
    core = new Core(db, util, log)
    core.restart = stub()
    db.write = stubWrite = stub().resolves()
    log.info.reset()
    log.warn.reset()
    log.error.reset()
    testApp = {
      name: testAppName,
      fresh: false,
      ctx: {
        log: {
          info: stub(),
          warn: stub(),
          error: stub(),
        },
      },
      closeServer: stub(),
      runVersion: stub(),
    }
    core.applicationMap.set(testAppName, testApp)
  })

  t.test('Should always attempt to close application first', async function() {
    const assertAppName = 'Pyramid Collapse'
    const assertError = new Error('Pyramid Collapse')
    const stubClose = stub().rejects(assertError)
    db.data.core[assertAppName] = {
      versions: [{installed: true, stable: 0}]
    }
    core.applicationMap.set(assertAppName, {
      name: assertAppName,
      closeServer: stubClose,
    })

    let err = await assert.isRejected(core.runApplication(core.applicationMap.get(assertAppName)))

    assert.strictEqual(err, assertError)
  })

  t.test('should select first valid version', async function() {
    const assertVersion = 'v50'
    const assertError = new Error('jellyfish')
    testApp.runVersion.rejects(assertError)
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: assertVersion,
      version: assertVersion + 'asdf',
      installed: true,
      stable: 0,
    })

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.notStrictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)

    assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError)
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertVersion))
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
    assert.strict(testApp.runVersion.firstCall[0], assertVersion)
    assert.match(err.message, /no/i)
    assert.match(err.message, /found/i)
  })

  t.test('should skip versions that have not been installed or high error', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.rejects(assertError)
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '40',
      version: 'v40',
      installed: false,
      stable: 0,
    }, {
      id: '41',
      version: 'v41',
      installed: true,
      stable: -2,
    }, {
      id: '42',
      version: 'v42',
      installed: true,
      stable: 0,
    }, )

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.notStrictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)

    assert.strictEqual(testApp.ctx.log.error.callCount, 1)
    assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError)
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('42'))
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
    assert.strict(testApp.runVersion.firstCall[0], '42')
  })
  
  t.test('should attempt to run version with stable of -1 if fresh is true', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.returnWith(function() {
      testApp.fresh = false
      return Promise.reject(assertError)
    })
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '30',
      version: 'v30',
      installed: false,
      stable: 0,
    }, {
      id: '31',
      version: 'v31',
      installed: true,
      stable: -1,
    }, {
      id: '32',
      version: 'v32',
      installed: true,
      stable: 0,
    }, )

    await core.runApplication(testApp)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)

    assert.strictEqual(testApp.ctx.log.error.callCount, 2)
    assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError)
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('31'))
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
    assert.strict(testApp.runVersion.firstCall[0], '31')
    assert.strict(testApp.runVersion.firstCall[0], '32')
    assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2)
    assert.strictEqual(db.data.core[testAppName].versions[2].stable, -1)
  })
  
  t.test('should call restart if program crashes and fresh is false', async function() {
    const assertErrorMessage = new Error('Daikichi to Rin')
    const assertError = new Error('Country Lane')
    testApp.runVersion.rejects(assertErrorMessage)
    core.restart.rejects(assertError)
    testApp.fresh = false
    db.data.core[testAppName].versions.push({
      id: '30',
      version: 'v30',
      installed: true,
      stable: 0,
    }, {
      id: '31',
      version: 'v31',
      installed: true,
      stable: -1,
    }, {
      id: '32',
      version: 'v32',
      installed: true,
      stable: 0,
    }, )

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.strictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)

    assert.strictEqual(testApp.ctx.log.error.callCount, 1)
    assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertErrorMessage)
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('30'))
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertErrorMessage.message))
    assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
    assert.ok(core.restart.called)
    assert.match(core.restart.firstCall[0], new RegExp(testAppName))
    assert.match(core.restart.firstCall[0], /v30/)
    assert.match(core.restart.firstCall[0], /dirty/)
  })
  
  t.test('should attempt next non-tested version if fresh is true', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.rejects(assertError)
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '30',
      version: 'v30',
      installed: true,
      stable: 0,
    }, {
      id: '31',
      version: 'v31',
      installed: true,
      stable: -1,
    }, {
      id: '32',
      version: 'v32',
      installed: true,
      stable: -2,
    })

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.notStrictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)

    assert.strictEqual(testApp.ctx.log.error.callCount, 2)
    assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError)
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('30'))
    assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
    assert.match(testApp.ctx.log.error.secondCall[1], new RegExp('31'))
    assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message))

    assert.strictEqual(db.data.core[testAppName].versions[0].stable, -2)
    assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2)

    assert.ok(stubWrite.callCount, 2)
  })
  
  t.test('should exit immediately if next version is -1 and fresh is false', async function() {
    const assertError = new Error('Daikichi to Rin')
    core.restart.rejects(assertError)
    testApp.fresh = false
    db.data.core[testAppName].versions.push({
      id: '30',
      version: 'v30',
      installed: true,
      stable: -1,
    }, {
      id: '31',
      version: 'v31',
      installed: true,
      stable: 0,
    }, {
      id: '32',
      version: 'v32',
      installed: true,
      stable: -2,
    })

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.strictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.warn.called)

    assert.strictEqual(testApp.ctx.log.warn.callCount, 1)
    assert.match(testApp.ctx.log.warn.firstCall[0], /restart/i)
    assert.match(testApp.ctx.log.warn.firstCall[0], /fresh/i)
    assert.match(testApp.ctx.log.warn.firstCall[0], /v30/i)
    assert.match(core.restart.firstCall[0], /v30/i)
    assert.match(core.restart.firstCall[0], /fresh/i)
    assert.match(core.restart.firstCall[0], /-1/i)
    assert.match(core.restart.firstCall[0], new RegExp(testAppName))
    
    assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
    assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0)
  })
  
  t.test('should stop on first stable and call core.restart if crash occurs', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.rejects(new Error('empty message'))
    core.restart.rejects(assertError)
    testApp.fresh = false
    db.data.core[testAppName].versions.push({
      id: '28',
      version: 'v28',
      installed: true,
      stable: 5,
    }, {
      id: '29',
      version: 'v29',
      installed: true,
      stable: 1,
    }, {
      id: '30',
      version: 'v30',
      installed: true,
      stable: 0,
    }, {
      id: '31',
      version: 'v31',
      installed: true,
      stable: -1,
    })

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.strictEqual(err, assertError)
    assert.notOk(log.error.called)
    assert.ok(testApp.ctx.log.error.called)
    assert.strictEqual(testApp.ctx.log.error.callCount, 1)
    assert.strictEqual(db.data.core[testAppName].versions[0].stable, 5)
    assert.strictEqual(stubWrite.callCount, 1)
    assert.ok(core.restart.called)
    assert.match(core.restart.firstCall[0], new RegExp(testAppName))
    assert.match(core.restart.firstCall[0], /v28/)
    assert.match(core.restart.firstCall[0], /stable/)
  })

  t.test('should throw if no stable version is found', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.rejects(assertError)
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '20',
      version: 'v20',
      installed: true,
      stable: 0,
    }, {
      id: '21',
      version: 'v21',
      installed: true,
      stable: 0,
    }, {
      id: '22',
      version: 'v22',
      installed: true,
      stable: 0,
    })

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.notStrictEqual(err, assertError)
    assert.match(err.message, /found/)
  })

  t.test('should throw if no version are found', async function() {
    const assertError = new Error('Daikichi to Rin')
    testApp.runVersion.rejects(assertError)
    testApp.fresh = true
    db.data.core[testAppName].versions = []

    let err = await assert.isRejected(core.runApplication(testApp))
    assert.notStrictEqual(err, assertError)
    assert.match(err.message, /found/)
    assert.notMatch(err.message, /stable/i)
  })
  
  t.test('should otherwise succeed if one is found', async function() {
    const assertError = new Error('Daikichi to Rin')
    let count = 0
    testApp.runVersion.returnWith(function() {
      if (count > 0) return Promise.reject(assertError)
      count++
      return Promise.resolve()
    })
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '70',
      version: 'v70',
      installed: true,
      stable: 100,
    }, {
      id: '71',
      version: 'v71',
      installed: true,
      stable: 0,
    }, {
      id: '72',
      version: 'v72',
      installed: true,
      stable: 0,
    })

    await core.runApplication(testApp)
    assert.notOk(log.error.called)
    assert.notOk(testApp.ctx.log.error.called)
    assert.strictEqual(testApp.runVersion.callCount, 1)
    assert.strictEqual(testApp.runVersion.firstCall[0], 'v70')
    assert.strictEqual(db.data.core[testAppName].versions[0].stable, 1)
    assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0)
    assert.strictEqual(db.data.core[testAppName].versions[2].stable, 0)
    assert.ok(stubWrite.called)
  })
  
  t.test('should succeed if running a minus one on fresh', async function() {
    const assertError = new Error('Daikichi to Rin')
    let count = 0
    testApp.fresh = true
    testApp.runVersion.returnWith(function() {
      if (count > 0) return Promise.reject(assertError)
      count++
      return Promise.resolve()
    })
    testApp.fresh = true
    db.data.core[testAppName].versions.push({
      id: '70',
      version: 'v70',
      installed: true,
      stable: -1,
    }, {
      id: '71',
      version: 'v71',
      installed: true,
      stable: 0,
    }, {
      id: '72',
      version: 'v72',
      installed: true,
      stable: 0,
    })

    await core.runApplication(testApp)
    assert.notOk(log.error.called)
    assert.notOk(testApp.ctx.log.error.called)
    assert.strictEqual(testApp.runVersion.callCount, 1)
    assert.strictEqual(testApp.runVersion.firstCall[0], 'v70')
    assert.strictEqual(db.data.core[testAppName].versions[0].stable, 1)
    assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0)
    assert.strictEqual(db.data.core[testAppName].versions[2].stable, 0)
    assert.ok(stubWrite.called)
  })
})

t.describe('#criticalError()', function() {
  let core
  let testApp
  let testAppName
  let stubWrite

  t.beforeEach(function() {
    testAppName = 'nano.RIPE'
    core = new Core(db, util, log)
    db.writeSync = stubWrite = stub()
    log.info.reset()
    log.warn.reset()
    log.error.reset()
    testApp = {
      name: testAppName,
      fresh: false,
      ctx: {
        log: {
          info: stub(),
          warn: stub(),
          error: stub(),
          fatal: stub(),
        },
      },
      closeServer: stub(),
      runVersion: stub(),
    }
  })

  t.test('should log to fatal', function() {
    const assertVersion = {
      version: 'Dai kirai! Aishiteru',
      stable: 0,
    }
    assert.notOk(testApp.ctx.log.fatal.called)
    core.criticalError(testApp, assertVersion)
    assert.ok(testApp.ctx.log.fatal.called)
    assert.match(testApp.ctx.log.fatal.firstCall[0], /critical/i)
    assert.match(testApp.ctx.log.fatal.firstCall[0], new RegExp(assertVersion.version))
  })

  t.test('should always change to stable -2 regardless of fresh', function() {
    const assertVersion = {
      version: 'Dai kirai! Aishiteru',
      stable: 0,
    }
    testApp.fresh = false

    assertVersion.stable = 5
    core.criticalError(testApp, assertVersion)
    assert.strictEqual(assertVersion.stable, -2)

    assertVersion.stable = -1
    core.criticalError(testApp, assertVersion)
    assert.strictEqual(assertVersion.stable, -2)

    testApp.fresh = true
    
    assertVersion.stable = 5
    core.criticalError(testApp, assertVersion)
    assert.strictEqual(assertVersion.stable, -2)

    assertVersion.stable = -1
    core.criticalError(testApp, assertVersion)
    assert.strictEqual(assertVersion.stable, -2)
  })

  t.test('should call db.writeSync afterwards', function() {
    let checkStable = 0
    const assertVersion = {
      version: 'Dai kirai! Aishiteru',
      stable: 0,
    }

    stubWrite.returnWith(function() {
      checkStable = assertVersion.stable
    })

    assert.notOk(stubWrite.called)
    core.criticalError(testApp, assertVersion)
    assert.ok(stubWrite.called)
    assert.strictEqual(checkStable, -2)
  })
})