import { Video, VideoPrivacy } from '@peertube/peertube-models'
import {
  createSingleServer,
  doubleFollow,
  killallServers,
  PeerTubeServer,
  setAccessTokensToServers,
  waitJobs
} from '@peertube/peertube-server-commands'
import autocannon, { printResult } from 'autocannon'
import { program } from 'commander'
import { writeJson } from 'fs-extra/esm'

let servers: PeerTubeServer[]
// First server
let server: PeerTubeServer
let video: Video
let threadId: number

program
  .option('-o, --outfile [outfile]', 'Outfile')
  .option('--grep [string]', 'Filter tests you want to execute')
  .description('Run API REST benchmark')
  .parse(process.argv)

const options = program.opts()

const outfile = options.outfile

run()
  .catch(err => console.error(err))
  .finally(() => {
    if (servers) return killallServers(servers)
  })

function buildAuthorizationHeader () {
  return {
    Authorization: 'Bearer ' + server.accessToken
  }
}

function buildAPHeader () {
  return {
    Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
  }
}

function buildJSONHeader () {
  return {
    'Content-Type': 'application/json'
  }
}

async function run () {
  console.log('Preparing server...')

  await prepare()

  const tests = [
    {
      title: 'AP - account peertube',
      path: '/accounts/peertube',
      headers: buildAPHeader(),
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"@context":')
      }
    },
    {
      title: 'AP - video',
      path: '/videos/watch/' + video.uuid,
      headers: buildAPHeader(),
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"@context":')
      }
    },
    {
      title: 'Misc - webfinger peertube',
      path: '/.well-known/webfinger?resource=acct:peertube@' + server.host,
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"subject":')
      }
    },
    {
      title: 'API - unread notifications',
      path: '/api/v1/users/me/notifications?start=0&count=0&unread=true',
      headers: buildAuthorizationHeader(),
      expecter: (_body, status) => {
        return status === 200
      }
    },
    {
      title: 'API - me',
      path: '/api/v1/users/me',
      headers: buildAuthorizationHeader(),
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"id":')
      }
    },
    {
      title: 'API - videos list',
      path: '/api/v1/videos',
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"total":10')
      }
    },
    {
      title: 'API - videos list without count*',
      path: '/api/v1/videos?skipCount=true',
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"data":[{"')
      }
    },
    {
      title: 'API - video get',
      path: '/api/v1/videos/' + video.uuid,
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"id":')
      }
    },
    {
      title: 'API - video captions',
      path: '/api/v1/videos/' + video.uuid + '/captions',
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"total":4')
      }
    },
    {
      title: 'API - video threads',
      path: '/api/v1/videos/' + video.uuid + '/comment-threads',
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"total":10')
      }
    },
    {
      title: 'API - video replies',
      path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId,
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"comment":{')
      }
    },
    {
      title: 'HTML - video watch',
      path: '/videos/watch/' + video.uuid,
      expecter: (body, status) => {
        return status === 200 && body.includes('<title>my super')
      }
    },
    {
      title: 'HTML - video embed',
      path: '/videos/embed/' + video.uuid,
      expecter: (body, status) => {
        return status === 200 && body.includes('embed')
      }
    },
    {
      title: 'HTML - homepage',
      path: '/',
      expecter: (_body, status) => {
        return status === 200
      }
    },
    {
      title: 'API - config',
      path: '/api/v1/config',
      expecter: (body, status) => {
        return status === 200 && body.startsWith('{"client":')
      }
    },
    {
      title: 'API - views with token',
      method: 'PUT',
      headers: {
        ...buildAuthorizationHeader(),
        ...buildJSONHeader()
      },
      body: JSON.stringify({ currentTime: 2 }),
      path: '/api/v1/videos/' + video.uuid + '/views',
      expecter: (body, status) => {
        return status === 204
      }
    },
    {
      title: 'API - views without token',
      method: 'POST',
      headers: buildJSONHeader(),
      body: JSON.stringify({ currentTime: 2 }),
      path: '/api/v1/videos/' + video.uuid + '/views',
      expecter: (body, status) => {
        return status === 204
      }
    }
  ].filter(t => {
    if (!options.grep) return true

    return t.title.includes(options.grep)
  })

  const finalResult: any[] = []

  for (const test of tests) {
    console.log('Running against %s.', test.path)
    const testResult = await runBenchmark(test)

    Object.assign(testResult, { title: test.title, path: test.path })
    finalResult.push(testResult)

    console.log(printResult(testResult))
  }

  if (outfile) await writeJson(outfile, finalResult)
}

function runBenchmark (options: {
  path: string
  method?: string
  body?: string
  headers?: { [id: string]: string }
  expecter: Function
}) {
  const { method = 'GET', path, body, expecter, headers } = options

  return new Promise((res, rej) => {
    autocannon({
      url: server.url + path,
      method,
      body,
      connections: 20,
      headers,
      pipelining: 1,
      duration: 10,
      requests: [
        {
          onResponse: (status, body) => {
            if (expecter(body, status) !== true) {
              console.error('Expected result failed.', { body, status })
              throw new Error('Invalid expectation')
            }
          }
        }
      ]
    }, (err, result) => {
      if (err) return rej(err)

      return res(result)
    })
  })
}

async function prepare () {
  const config = {
    rates_limit: {
      api: {
        max: 5_000_000
      },
      login: {
        max: 5_000_000
      },
      signup: {
        max: 5_000_000
      },
      ask_send_email: {
        max: 5_000_000
      },
      receive_client_log: {
        max: 5_000_000
      },
      plugins: {
        max: 5_000_000
      },
      well_known: {
        max: 5_000_000
      },
      feeds: {
        max: 5_000_000
      },
      activity_pub: {
        max: 5_000_000
      },
      client: {
        max: 5_000_000
      }
    }
  }

  servers = await Promise.all([
    createSingleServer(1, config, { nodeArgs: [ '--inspect' ] }),
    createSingleServer(2, config),
    createSingleServer(3, config)
  ])
  server = servers[0]

  await setAccessTokensToServers(servers)
  await doubleFollow(servers[0], servers[1])
  await doubleFollow(servers[0], servers[2])

  const attributes = {
    name: 'my super video',
    category: 2,
    nsfw: true,
    licence: 6,
    language: 'fr',
    privacy: VideoPrivacy.PUBLIC,
    support: 'please give me a coffee',
    description: 'my super description\n'.repeat(10) + ' * list1\n * list 2\n * list 3',
    tags: [ 'tag1', 'tag2', 'tag3' ]
  }

  for (let i = 0; i < 10; i++) {
    await server.videos.upload({ attributes: { ...attributes, name: 'my super video ' + i } })
  }

  const { data } = await server.videos.list()
  video = data.find(v => v.name === 'my super video 1')

  for (let i = 0; i < 10; i++) {
    const text = 'my super first comment'
    const created = await server.comments.createThread({ videoId: video.id, text })
    threadId = created.id

    const text1 = 'my super answer to thread 1'
    const child = await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text1 })

    const text2 = 'my super answer to answer of thread 1'
    await server.comments.addReply({ videoId: video.id, toCommentId: child.id, text: text2 })

    const text3 = 'my second answer to thread 1'
    await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text3 })
  }

  for (const caption of [ 'ar', 'fr', 'en', 'zh' ]) {
    await server.captions.add({
      language: caption,
      videoId: video.id,
      fixture: 'subtitle-good2.vtt'
    })
  }

  await waitJobs(servers)
}
