/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */

import { expect } from 'chai'
import { wait } from '@shared/core-utils'
import {
  cleanupTests,
  createMultipleServers,
  doubleFollow,
  PeerTubeServer,
  setAccessTokensToServers,
  waitJobs
} from '@shared/server-commands'

describe('Test AP cleaner', function () {
  let servers: PeerTubeServer[] = []
  let videoUUID1: string
  let videoUUID2: string
  let videoUUID3: string

  let videoUUIDs: string[]

  before(async function () {
    this.timeout(120000)

    const config = {
      federation: {
        videos: { cleanup_remote_interactions: true }
      }
    }
    servers = await createMultipleServers(3, config)

    // Get the access tokens
    await setAccessTokensToServers(servers)

    await Promise.all([
      doubleFollow(servers[0], servers[1]),
      doubleFollow(servers[1], servers[2]),
      doubleFollow(servers[0], servers[2])
    ])

    // Update 1 local share, check 6 shares

    // Create 1 comment per video
    // Update 1 remote URL and 1 local URL on

    videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid
    videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
    videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid

    videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ]

    await waitJobs(servers)

    for (const server of servers) {
      for (const uuid of videoUUIDs) {
        await server.videos.rate({ id: uuid, rating: 'like' })
        await server.comments.createThread({ videoId: uuid, text: 'comment' })
      }
    }

    await waitJobs(servers)
  })

  it('Should have the correct likes', async function () {
    for (const server of servers) {
      for (const uuid of videoUUIDs) {
        const video = await server.videos.get({ id: uuid })

        expect(video.likes).to.equal(3)
        expect(video.dislikes).to.equal(0)
      }
    }
  })

  it('Should destroy server 3 internal likes and correctly clean them', async function () {
    this.timeout(20000)

    await servers[2].sql.deleteAll('accountVideoRate')
    for (const uuid of videoUUIDs) {
      await servers[2].sql.setVideoField(uuid, 'likes', '0')
    }

    await wait(5000)
    await waitJobs(servers)

    // Updated rates of my video
    {
      const video = await servers[0].videos.get({ id: videoUUID1 })
      expect(video.likes).to.equal(2)
      expect(video.dislikes).to.equal(0)
    }

    // Did not update rates of a remote video
    {
      const video = await servers[0].videos.get({ id: videoUUID2 })
      expect(video.likes).to.equal(3)
      expect(video.dislikes).to.equal(0)
    }
  })

  it('Should update rates to dislikes', async function () {
    this.timeout(20000)

    for (const server of servers) {
      for (const uuid of videoUUIDs) {
        await server.videos.rate({ id: uuid, rating: 'dislike' })
      }
    }

    await waitJobs(servers)

    for (const server of servers) {
      for (const uuid of videoUUIDs) {
        const video = await server.videos.get({ id: uuid })
        expect(video.likes).to.equal(0)
        expect(video.dislikes).to.equal(3)
      }
    }
  })

  it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
    this.timeout(20000)

    await servers[2].sql.deleteAll('accountVideoRate')

    for (const uuid of videoUUIDs) {
      await servers[2].sql.setVideoField(uuid, 'dislikes', '0')
    }

    await wait(5000)
    await waitJobs(servers)

    // Updated rates of my video
    {
      const video = await servers[0].videos.get({ id: videoUUID1 })
      expect(video.likes).to.equal(0)
      expect(video.dislikes).to.equal(2)
    }

    // Did not update rates of a remote video
    {
      const video = await servers[0].videos.get({ id: videoUUID2 })
      expect(video.likes).to.equal(0)
      expect(video.dislikes).to.equal(3)
    }
  })

  it('Should destroy server 3 internal shares and correctly clean them', async function () {
    this.timeout(20000)

    const preCount = await servers[0].sql.getVideoShareCount()
    expect(preCount).to.equal(6)

    await servers[2].sql.deleteAll('videoShare')
    await wait(5000)
    await waitJobs(servers)

    // Still 6 because we don't have remote shares on local videos
    const postCount = await servers[0].sql.getVideoShareCount()
    expect(postCount).to.equal(6)
  })

  it('Should destroy server 3 internal comments and correctly clean them', async function () {
    this.timeout(20000)

    {
      const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
      expect(total).to.equal(3)
    }

    await servers[2].sql.deleteAll('videoComment')

    await wait(5000)
    await waitJobs(servers)

    {
      const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
      expect(total).to.equal(2)
    }
  })

  it('Should correctly update rate URLs', async function () {
    this.timeout(30000)

    async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
      const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
        `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
      const res = await servers[0].sql.selectQuery<{ url: string }>(query)

      for (const rate of res) {
        const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
        expect(rate.url).to.match(matcher)
      }
    }

    async function checkLocal () {
      const startsWith = 'http://' + servers[0].host + '%'
      // On local videos
      await check(startsWith, servers[0].url, '', 'false')
      // On remote videos
      await check(startsWith, servers[0].url, '', 'true')
    }

    async function checkRemote (suffix: string) {
      const startsWith = 'http://' + servers[1].host + '%'
      // On local videos
      await check(startsWith, servers[1].url, suffix, 'false')
      // On remote videos, we should not update URLs so no suffix
      await check(startsWith, servers[1].url, '', 'true')
    }

    await checkLocal()
    await checkRemote('')

    {
      const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
      await servers[1].sql.updateQuery(query)

      await wait(5000)
      await waitJobs(servers)
    }

    await checkLocal()
    await checkRemote('stan')
  })

  it('Should correctly update comment URLs', async function () {
    this.timeout(30000)

    async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
      const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
        `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`

      const res = await servers[0].sql.selectQuery<{ url: string, videoUUID: string }>(query)

      for (const comment of res) {
        const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
        expect(comment.url).to.match(matcher)
      }
    }

    async function checkLocal () {
      const startsWith = 'http://' + servers[0].host + '%'
      // On local videos
      await check(startsWith, servers[0].url, '', 'false')
      // On remote videos
      await check(startsWith, servers[0].url, '', 'true')
    }

    async function checkRemote (suffix: string) {
      const startsWith = 'http://' + servers[1].host + '%'
      // On local videos
      await check(startsWith, servers[1].url, suffix, 'false')
      // On remote videos, we should not update URLs so no suffix
      await check(startsWith, servers[1].url, '', 'true')
    }

    {
      const query = `UPDATE "videoComment" SET url = url || 'kyle'`
      await servers[1].sql.updateQuery(query)

      await wait(5000)
      await waitJobs(servers)
    }

    await checkLocal()
    await checkRemote('kyle')
  })

  it('Should remove unavailable remote resources', async function () {
    this.timeout(240000)

    async function expectNotDeleted () {
      {
        const video = await servers[0].videos.get({ id: uuid })

        expect(video.likes).to.equal(3)
        expect(video.dislikes).to.equal(0)
      }

      {
        const { total } = await servers[0].comments.listThreads({ videoId: uuid })
        expect(total).to.equal(3)
      }
    }

    async function expectDeleted () {
      {
        const video = await servers[0].videos.get({ id: uuid })

        expect(video.likes).to.equal(2)
        expect(video.dislikes).to.equal(0)
      }

      {
        const { total } = await servers[0].comments.listThreads({ videoId: uuid })
        expect(total).to.equal(2)
      }
    }

    const uuid = (await servers[0].videos.quickUpload({ name: 'server 1 video 2' })).uuid

    await waitJobs(servers)

    for (const server of servers) {
      await server.videos.rate({ id: uuid, rating: 'like' })
      await server.comments.createThread({ videoId: uuid, text: 'comment' })
    }

    await waitJobs(servers)

    await expectNotDeleted()

    await servers[1].kill()

    await wait(5000)
    await expectNotDeleted()

    let continueWhile = true

    do {
      try {
        await expectDeleted()
        continueWhile = false
      } catch {
      }
    } while (continueWhile)
  })

  after(async function () {
    await cleanupTests(servers)
  })
})
