import sharp from 'sharp'
import { Eltro as t, assert} from 'eltro'
import fs from 'fs/promises'
import { fileURLToPath } from 'url'
import path from 'path'

import { createClient, startServer, log, resetLog } from '../helper.server.mjs'
import encode from '../../api/jwt/encode.mjs'

let __dirname = path.dirname(fileURLToPath(import.meta.url))

function resolve(file) {
  return path.resolve(path.join(__dirname, file))
}

const currYear = new Date().getFullYear().toString()

t.describe('Media (API)', () => {
  let client
  let secret = 'asdf1234'
  let testFiles = []

  t.before(function() {
    client = createClient()
    return startServer()
  })

  t.after(function() {
    return Promise.all(testFiles.map(function(file) {
      return fs.unlink(resolve(`../../public/${file}`)).catch(function() {})
    }))
    .then(function() {
      // return fs.unlink(resolve('../../public/existing/test.png')).catch(function() {})
    })
  })

  t.timeout(10000).describe('POST /media', function temp() {
    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(
        client.upload('/media',
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(
        client.upload('/media?token=' + assertToken,
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should upload file and create file', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media?token=${token}`,
          resolve('test.png')
        )
      )

      testFiles.push(data.path)

      assert.ok(data)
      assert.ok(data.filename)
      assert.ok(data.filename.startsWith(currYear))
      assert.ok(data.path)

      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve(`../../public/${data.path}`)),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)

      let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')
    })
  })

  t.timeout(10000).describe('POST /media/noprefix', function temp() {
    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(
        client.upload('/media/noprefix',
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(
        client.upload('/media/noprefix?token=' + assertToken,
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should upload and create file with no prefix', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media/noprefix?token=${token}`,
          resolve('test.png')
        )
      )

      testFiles.push(data.path)

      assert.ok(data)
      assert.strictEqual(data.filename, 'test.png')
      assert.ok(data.path)

      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve('../../public/development/test.png')),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)

      let img = await sharp(resolve('../../public/development/test.png')).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')
    })
    
    t.test('should upload and create file with prefix if exists', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      await fs.unlink(resolve(`../../public/development/test.png`)).catch(function() {})
      
      let datas = []
      datas.push(await client.upload(
        `/media/noprefix?token=${token}`,
        resolve('test.png')
      ))

      datas.push(await client.upload(
        `/media/noprefix?token=${token}`,
        resolve('test.png')
      ))

      testFiles.push(datas[0].path)
      testFiles.push(datas[1].path)

      let prefix = new Date().toISOString().replace(/-/g,'').split('T')[0]

      let first = datas[0]
      let second = datas[1]

      assert.notStrictEqual(first.filename, second.filename)
      assert.notOk(first.filename.startsWith(currYear))
      assert.notOk(first.filename.startsWith(prefix))
      assert.ok(second.filename.startsWith(currYear))
      assert.ok(second.filename.startsWith(prefix))

      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve('../../public/development/test.png')),
        fs.stat(resolve('../../public/development/' + second.filename)),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)
      assert.strictEqual(stats[0].size, stats[2].size)
    })
  })

  t.timeout(10000).describe('POST /media/resize', function temp() {
    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(
        client.upload('/media/resize',
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(
        client.upload('/media/resize?token=' + assertToken,
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should upload file and create file', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media/resize?token=${token}`,
          resolve('test.png'),
          'POST',
          { }
        )
      )

      testFiles.push(data.path)

      assert.ok(data)
      assert.ok(data.filename)
      assert.ok(data.filename.startsWith(currYear))
      assert.ok(data.path)

      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve(`../../public/${data.path}`)),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)

      let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')
    })

    t.test('should upload file and create multiple sizes for file', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media/resize?token=${token}`,
          resolve('test.png'),
          'POST',
          {
            test1: {
              format: 'jpeg',
              resize: {
                width: 300,
              },
              blur: 0.5,
              flatten: {r:0,g:0,b:0},
              trim: 1,
              extend: { top: 10, left: 10, bottom: 10, right: 10, background: {r:0,g:0,b:0} },
              jpeg: {
                quality: 80,
                mozjpeg: true,
              }
            },
            test2: {
              format: 'png',
              resize: {
                width: 150,
              },
              png: {
                compressionLevel: 9,
              }
            },
          }
        )
      )
      
      testFiles.push(data.path)
      testFiles.push(data.test1.path)
      testFiles.push(data.test2.path)

      assert.ok(data)
      assert.ok(data.filename)
      assert.ok(data.filename.startsWith(currYear))
      assert.ok(data.path)
      assert.ok(data.test1.filename)
      assert.ok(data.test1.filename.startsWith(currYear))
      assert.ok(data.test1.path)
      assert.ok(data.test2.filename)
      assert.ok(data.test2.filename.startsWith(currYear))
      assert.ok(data.test2.path)


      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve(`../../public/${data.path}`)),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)

      let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')
      
      img = await sharp(resolve(`../../public/${data.test1.path}`)).metadata()
      assert.strictEqual(img.width, 320)
      assert.strictEqual(img.height, 413)
      assert.strictEqual(img.format, 'jpeg')
      
      img = await sharp(resolve(`../../public/${data.test2.path}`)).metadata()
      assert.strictEqual(img.width, 150)
      assert.strictEqual(img.height, 175)
      assert.strictEqual(img.format, 'png')
    })

    t.test('should upload file and support base64 output', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media/resize?token=${token}`,
          resolve('test.png'),
          'POST',
          {
            outtest: {
              out: 'base64',
              format: 'jpeg',
              resize: {
                width: 10,
              },
              jpeg: {
                quality: 80,
                mozjpeg: true,
              }
            },
          }
        )
      )

      assert.ok(data)
      assert.ok(data.filename)
      assert.ok(data.path)
      assert.ok(data.outtest.base64)

      
      testFiles.push(data.path)

      let stats = await Promise.all([
        fs.stat(resolve('test.png')),
        fs.stat(resolve(`../../public/${data.path}`)),
      ])
      assert.strictEqual(stats[0].size, stats[1].size)

      let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')

      let bufferBase64 = Buffer.from(data.outtest.base64.slice(data.outtest.base64.indexOf(',')), 'base64')
      
      img = await sharp(bufferBase64).metadata()
      assert.strictEqual(img.width, 10)
      assert.strictEqual(img.height, 12)
      assert.strictEqual(img.format, 'jpeg')
    })
  })

  t.timeout(10000).describe('POST /media/resize/:filename', function temp() {
    let sourceFilename
    let sourcePath
    
    t.before(async function() {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.upload(
          `/media/resize?token=${token}`,
          resolve('test.png'),
          'POST',
          { }
        )
      )

      testFiles.push(data.path)

      assert.ok(data)
      assert.ok(data.filename)
      assert.ok(data.filename.startsWith(currYear))
      assert.ok(data.path)

      sourceFilename = data.filename
      sourcePath = data.path
    })

    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(
        client.post(`/media/resize/${sourceFilename}`, {})
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(
        client.post(`/media/resize/${sourceFilename}?token=${assertToken}`, {})
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should create multiple sizes for existing file', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.post(
          `/media/resize/${sourceFilename}?token=${token}`,
          {
            test1: {
              format: 'jpeg',
              resize: {
                width: 300,
              },
              blur: 0.5,
              flatten: {r:0,g:0,b:0},
              trim: 1,
              extend: { top: 10, left: 10, bottom: 10, right: 10, background: {r:0,g:0,b:0} },
              jpeg: {
                quality: 80,
                mozjpeg: true,
              }
            },
            test2: {
              format: 'png',
              resize: {
                width: 150,
              },
              png: {
                compressionLevel: 9,
              }
            },
          }
        )
      )
      
      testFiles.push(data.test1.path)
      testFiles.push(data.test2.path)

      assert.ok(data.test1.filename)
      assert.ok(data.test1.filename.startsWith(currYear))
      assert.ok(data.test1.path)
      assert.ok(data.test2.filename)
      assert.ok(data.test2.filename.startsWith(currYear))
      assert.ok(data.test2.path)

      let img = await sharp(resolve(`../../public/${sourcePath}`)).metadata()
      assert.strictEqual(img.width, 600)
      assert.strictEqual(img.height, 700)
      assert.strictEqual(img.format, 'png')
      
      img = await sharp(resolve(`../../public/${data.test1.path}`)).metadata()
      assert.strictEqual(img.width, 320)
      assert.strictEqual(img.height, 413)
      assert.strictEqual(img.format, 'jpeg')
      
      img = await sharp(resolve(`../../public/${data.test2.path}`)).metadata()
      assert.strictEqual(img.width, 150)
      assert.strictEqual(img.height, 175)
      assert.strictEqual(img.format, 'png')
    })

    t.test('should base64 output of existing file', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await assert.isFulfilled(
        client.post(
          `/media/resize/${sourceFilename}?token=${token}`,
          {
            outtest: {
              out: 'base64',
              format: 'jpeg',
              resize: {
                width: 10,
              },
              jpeg: {
                quality: 80,
                mozjpeg: true,
              }
            },
          }
        )
      )

      assert.ok(data)
      assert.ok(data.outtest.base64)

      let bufferBase64 = Buffer.from(data.outtest.base64.slice(data.outtest.base64.indexOf(',')), 'base64')
      
      let img = await sharp(bufferBase64).metadata()
      assert.strictEqual(img.width, 10)
      assert.strictEqual(img.height, 12)
      assert.strictEqual(img.format, 'jpeg')
    })
  })

  t.timeout(10000).describe('DELETE /media/:filename', function temp() {
    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(
        client.del('/media/20220105_101610_test1.jpg',
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(
        client.del('/media/20220105_101610_test1.jpg?token=' + assertToken,
          resolve('test.png')
        )
      )

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should remove the file', async () => {
      let token = encode(null, { iss: 'existing' }, secret)

      let data = await client.upload(
        `/media/noprefix?token=${token}`,
        resolve('test.png')
      )

      let filepath = data.path
      testFiles.push(filepath)

      let files = await client.get('/media/existing')

      let found = false
      for (let file of files) {
        if (file.filename === 'test.png') {
          found = true
        }
      }
      assert.ok(found)
      
      await assert.isFulfilled(
        fs.stat(resolve(`../../public/${filepath}`))
      )

      await assert.isFulfilled(
        client.del(`/media/test.png?token=${token}`)
      )

      testFiles.splice(testFiles.length - 1)

      files = await client.get('/media/existing')

      found = false
      for (let file of files) {
        if (file.filename === 'test.png') {
          found = true
        }
      }
      assert.notOk(found)

      await assert.isRejected(
        fs.stat(resolve(`../../public/${filepath}`))
      )
    })
  })

  t.describe('GET /media', function() {
    t.test('should require authentication', async () => {
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      let err = await assert.isRejected(client.get('/media'))

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Mm]issing/)

      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Mm]issing/)
    })

    t.test('should verify token correctly', async () => {
      const assertToken = 'asdf.asdf.asdf'
      resetLog()
      assert.strictEqual(log.error.callCount, 0)
      assert.strictEqual(log.warn.callCount, 0)
      assert.strictEqual(log.info.callCount, 0)

      let err = await assert.isRejected(client.get('/media?token=' + assertToken))

      assert.strictEqual(err.status, 422)
      assert.match(err.message, /[Tt]oken/)
      assert.match(err.message, /[Ii]nvalid/)

      assert.strictEqual(log.error.callCount, 1)
      assert.strictEqual(log.warn.callCount, 2)
      assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
      assert.match(log.warn.firstCall[0], /[Tt]oken/)
      assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
      assert.ok(log.error.lastCall[0] instanceof Error)
      assert.match(log.error.lastCall[1], new RegExp(assertToken))
    })

    t.test('should return list of files in specified folder', async () => {
      let token = encode(null, { iss: 'development' }, secret)

      let data = await client.get('/media?token=' + token)

      assert.ok(data.length)
      let found = false
      for (let file of data) {
        if (file.filename === '.gitkeep') {
          found = true
          break
        }
      }
      assert.ok(found)
    })
  })

  t.describe('GET /media/:site', function() {
    t.test('should give 404 on invalid sites', async () => {
      let err = await assert.isRejected(client.get('/media/development'))

      assert.strictEqual(err.status, 404)

      err = await assert.isRejected(client.get('/media/nonexisting'))

      assert.strictEqual(err.status, 404)

      err = await assert.isRejected(client.get('/media/blabla'))

      assert.strictEqual(err.status, 404)
    })

    t.test('should otherwise return list of files for a public site', async () => {
      let files = await client.get('/media/existing')
      assert.strictEqual(files[0].filename, '20220105_101610_test1.jpg')
      assert.strictEqual(files[0].size, null)
      assert.strictEqual(files[1].filename, '20220105_101610_test2.png')
      assert.strictEqual(files[1].size, null)
    })
  })
})
