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

const util = new Util(import.meta.url)

t.describe('#runVersion("static")', function() {
  const assertPort = 22345
  let ctx
  let app

  const defaultHandler = function(orgHandler) {
    let handler = orgHandler || function (req, res) {
      res.writeHead(200); res.end(JSON.stringify({ a: 1 }))
    }
    return function(http, port, ctx) {
      const server = http.createServer(handler)

      return server.listenAsync(port)
    }
  }
  
  t.beforeEach(function() {
    return createFakeContext()
      .then(function(res) {
        ctx = res
        let provider = new StaticProvider()
        app = new Application(ctx, provider, 'testapp')
        app.config.port = assertPort
        app.registerModule(defaultHandler())
      })
  })

  t.afterEach(function() {
    return app.closeServer()
  })

  t.test('should throw if http is not called', async function() {
    app.registerModule(function(checkHttp, checkPort, checkCtx) {
      assert.strictEqual(checkHttp, app.http)
      assert.strictEqual(checkPort, assertPort)
      assert.strictEqual(checkCtx.db, ctx.db)
      assert.strictEqual(checkCtx.log, ctx.log)
      assert.strictEqual(checkCtx.app, app)
    })

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('static'))
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, '')

    assert.match(err.message, /http/i)
    assert.match(err.message, /createServer/i)
    assert.match(err.message, /call/i)
    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })

  t.test('should throw if it timeouts waiting for promise to succeed', async function() {
    app.config.startWaitUntilFail = 50
    app.registerModule(function() { return new Promise(function() {}) })
    
    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('static'))
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, '')

    assert.match(err.message, /time/i)
    assert.match(err.message, /out/i)
    assert.match(err.message, /50ms/)
    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })

  t.test('should otherwise succeed if it finished within the time limit', async function() {
    const handler = defaultHandler()
    app.config.startWaitUntilFail = 250
    app.registerModule(function(http, port, ctx) {
      return new Promise(function(res) {
        setTimeout(res, 25)
      }).then(function() {
        return handler(http, port, ctx)
      })
    })
    
    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    await app.runVersion('static')
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, 'static')

    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })

  t.test('should fail if run succeeds but heartbeat errors', async function() {
    let called = 0
    const handler = function(req, res) {
      called++
      res.statusCode = 400
      res.end(JSON.stringify({ a: 1 }))
    }
    app.config.heartbeatAttempts = 3
    app.config.heartbeatAttemptsWait = 5
    app.registerModule(defaultHandler(handler))

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('static'))
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, '')

    assert.match(err.message, /failed/i)
    assert.match(err.message, /400/i)
    assert.strictEqual(called, 3)
    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })

  t.test('should fail if run succeeds but heartbeat times out', async function() {
    let called = 0
    const handler = function(req, res) {
      called++
    }
    app.config.heartbeatAttempts = 2
    app.config.heartbeatTimeout = 30
    app.config.heartbeatAttemptsWait = 10
    app.registerModule(defaultHandler(handler))

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let start = performance.now()
    let err = await assert.isRejected(app.runVersion('static'))
    let end = performance.now()
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, '')

    assert.match(err.message, /failed/i)
    assert.match(err.message, /time/i)
    assert.match(err.message, /out/i)
    assert.match(err.message, /30ms/i)
    assert.ok(end - start > app.config.heartbeatAttempts * (app.config.heartbeatTimeout + app.config.heartbeatAttemptsWait))
    assert.strictEqual(called, 2)
    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })

  t.test('should call with correct path', async function() {
    const assertPath = '/test/something'
    const handler = function(req, res) {
      if (req.url === assertPath) {
        res.writeHead(204); res.end(JSON.stringify({ a: 1 }))
      } else {
        res.statusCode = 400
        res.end(JSON.stringify({ a: 1 }))
      }
    }
    app.config.heartbeatAttempts = 3
    app.config.heartbeatAttemptsWait = 5
    app.registerModule(defaultHandler(handler))

    assert.strictEqual(app.ctx.version, '')
    let err = await assert.isRejected(app.runVersion('static'))
    assert.match(err.message, /failed/i)
    assert.match(err.message, /400/i)
    assert.strictEqual(app.ctx.version, '')

    await app.closeServer()
    app.registerModule(defaultHandler(handler))

    app.config.heartbeatPath = assertPath
    await app.runVersion('static')

    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })
  
  t.test('should not check heartbeat if slave', async function() {
    let called = 0
    const handler = function(req, res) {
      called++
      res.statusCode = 400
      res.end(JSON.stringify({ a: 1 }))
    }
    app.registerModule(defaultHandler(handler))
    app.isSlave = true

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    await app.runVersion('static')
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, 'static')

    assert.strictEqual(called, 0)
    assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
  })
})

t.describe('#runVersion("version")', function() {
  const assertConfig = util.getPathFromRoot('./db_test_applicationrun.json')
  const assertPort = 22345
  let ctx
  let app

  t.after(function() {
    return Promise.all([
      fs.rm(util.getPathFromRoot('./testnoexisting/v100'), { force: true, recursive: true }),
      fs.rm(util.getPathFromRoot('./testnoexisting/v99'), { force: true, recursive: true }),
      fs.rm(util.getPathFromRoot('./testnoexisting/v98'), { force: true, recursive: true }),
      fs.rm(util.getPathFromRoot('./testnoexisting/v97'), { force: true, recursive: true }),
    ])
  })

  t.beforeEach(function() {
    return createFakeContext({ }, util, assertConfig)
      .then(function(res) {
        ctx = res
        let provider = new StaticProvider()
        app = new Application(ctx, provider, 'testnoexisting')
        app.config.port = assertPort
        return app.ctx.db.write()
      })
  })

  t.afterEach(function() {
    return Promise.all([
      fs.rm(assertConfig),
      app.closeServer(),
    ])
  })

  t.test('when version is specified, should check if index.mjs exists', async function() {
    const assertNotError = new Error('AI DO')
    const assertTarget = util.getPathFromRoot('./testnoexisting/v100/index.mjs')
    let stubFsStat = stub()
    let provider = new StaticProvider()
    app = new Application(ctx, provider, 'testnoexisting', {
      fs: { stat: stubFsStat }
    })
    app.config.port = assertPort
    stubFsStat.rejects(assertNotError)

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('v100'))
    assert.strictEqual(app.fresh, true)
    assert.strictEqual(app.ctx.version, '')

    assert.notStrictEqual(err, assertNotError)
    assert.match(err.message, new RegExp(assertNotError.message))
    assert.match(err.message, /index\.mjs/i)
    assert.match(err.message, /missing/i)
    assert.strictEqual(stubFsStat.firstCall[0], assertTarget)

    assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v100')
    let checkDb = await lowdb({}, ctx.log, assertConfig)
    assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v100')
  })

  t.test('when version is specified and file exists, should attempt to load module', async function() {
    const assertError = new Error('Parallel Days')
    await fs.mkdir(util.getPathFromRoot('./testnoexisting/v99'), { recursive: true })
    await fs.writeFile(util.getPathFromRoot('./testnoexisting/v99/index.mjs'), `throw new Error('${assertError.message}')`)

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('v99'))
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, '')

    assert.notStrictEqual(err, assertError)
    assert.strictEqual(err.message, assertError.message)
    
    assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v99')
    let checkDb = await lowdb({}, ctx.log, assertConfig)
    assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v99')
  })

  t.test('when version is specified and file exists, should check if it has start', async function() {
    await fs.mkdir(util.getPathFromRoot('./testnoexisting/v98'), { recursive: true })
    await fs.writeFile(util.getPathFromRoot('./testnoexisting/v98/index.mjs'), ``)

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    let err = await assert.isRejected(app.runVersion('v98'))
    assert.strictEqual(app.fresh, false)
    assert.match(err.message, /start/i)
    assert.strictEqual(app.ctx.version, '')

    assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v98')
    let checkDb = await lowdb({}, ctx.log, assertConfig)
    assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v98')
  })

  t.test('when version is specified and file exists and everything is okay, should work normally', async function() {
    await fs.mkdir(util.getPathFromRoot('./testnoexisting/v97'), { recursive: true })
    await fs.copyFile(
      util.getPathFromRoot('./exampleindex.mjs'),
      util.getPathFromRoot('./testnoexisting/v97/index.mjs')
    )

    app.ctx.log.info.reset()
    app.ctx.log.event.info.reset()

    assert.strictEqual(app.ctx.version, '')
    assert.strictEqual(app.fresh, true)
    await app.runVersion('v97')
    assert.strictEqual(app.fresh, false)
    assert.strictEqual(app.ctx.version, 'v97')

    assert.ok(app.ctx.log.info.called)
    assert.ok(app.ctx.log.event.info.called)
  })
})
