/* eslint-disable @typescript-eslint/no-unused-expressions */
import { ffprobePromise } from '@peertube/peertube-ffmpeg'
import {
  ActivityCreate,
  ActivityPubOrderedCollection,
  HttpStatusCode,
  LiveVideoLatencyMode,
  UserExport,
  UserNotificationSettingValue,
  VideoCommentObject,
  VideoCommentPolicy,
  VideoObject,
  VideoPlaylistPrivacy,
  VideoPrivacy
} from '@peertube/peertube-models'
import { getFilenameFromUrl } from '@peertube/peertube-node-utils'
import {
  ConfigCommand,
  ObjectStorageCommand,
  PeerTubeServer,
  createSingleServer,
  doubleFollow,
  makeRawRequest,
  setAccessTokensToServers,
  setDefaultVideoChannel,
  waitJobs
} from '@peertube/peertube-server-commands'
import { expect } from 'chai'
import { ensureDir, remove } from 'fs-extra'
import { writeFile } from 'fs/promises'
import JSZip from 'jszip'
import { tmpdir } from 'os'
import { basename, join, resolve } from 'path'
import { testFileExistsOnFSOrNot } from './checks.js'
import { MockSmtpServer } from './mock-servers/mock-email.js'
import { getAllNotificationsSettings } from './notifications.js'

type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>>

export async function downloadZIP (server: PeerTubeServer, userId: number) {
  const { data } = await server.userExports.list({ userId })

  const res = await makeRawRequest({
    url: data[0].privateDownloadUrl,
    responseType: 'arraybuffer',
    redirects: 1,
    expectedStatus: HttpStatusCode.OK_200
  })

  return JSZip.loadAsync(res.body)
}

export async function parseZIPJSONFile<T> (zip: JSZip, path: string) {
  return JSON.parse(await zip.file(path).async('string')) as T
}

export async function checkFileExistsInZIP (zip: JSZip, path: string, base = '/') {
  const innerPath = resolve(base, path).substring(1) // Remove '/' at the beginning of the string

  expect(zip.files[innerPath], `${innerPath} does not exist`).to.exist

  const buf = await zip.file(innerPath).async('arraybuffer')
  expect(buf.byteLength, `${innerPath} is empty`).to.be.greaterThan(0)
}

export async function probeZIPFile (zip: JSZip, path: string, base = '/') {
  const innerPath = resolve(base, path).substring(1) // Remove '/' at the beginning of the string

  expect(zip.files[innerPath], `${innerPath} does not exist`).to.exist

  const buf = await zip.file(innerPath).async('arraybuffer')

  const basePath = join(tmpdir(), 'peertube-test')
  const videoPath = join(basePath, basename(innerPath))
  await ensureDir(basePath)
  await writeFile(videoPath, Buffer.from(buf))

  const probe = await ffprobePromise(videoPath)
  await remove(videoPath)

  return probe
}

// ---------------------------------------------------------------------------

export function parseAPOutbox (zip: JSZip) {
  return parseZIPJSONFile<ExportOutbox>(zip, 'activity-pub/outbox.json')
}

export function findVideoObjectInOutbox (outbox: ExportOutbox, videoName: string) {
  return outbox.orderedItems.find(i => {
    return i.type === 'Create' && i.object.type === 'Video' && i.object.name === videoName
  }) as ActivityCreate<VideoObject>
}

// ---------------------------------------------------------------------------

export async function regenerateExport (options: {
  server: PeerTubeServer
  userId: number
  withVideoFiles: boolean
}) {
  const { server, userId, withVideoFiles } = options

  await server.userExports.deleteAllArchives({ userId })
  const res = await server.userExports.request({ userId, withVideoFiles })
  await server.userExports.waitForCreation({ userId })

  return res
}

export async function checkExportFileExists (options: {
  server: PeerTubeServer
  userExport: UserExport
  redirectedUrl: string
  exists: boolean
  withObjectStorage: boolean
}) {
  const { server, exists, userExport, redirectedUrl, withObjectStorage } = options

  const filename = getFilenameFromUrl(userExport.privateDownloadUrl)

  if (exists === true) {
    if (withObjectStorage) {
      return makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.OK_200 })
    }

    return testFileExistsOnFSOrNot(server, 'tmp-persistent', filename, true)
  }

  await testFileExistsOnFSOrNot(server, 'tmp-persistent', filename, false)

  if (withObjectStorage) {
    await makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
  }
}

export async function prepareImportExportTests (options: {
  objectStorage: ObjectStorageCommand
  emails: object[]
  withBlockedServer: boolean
}) {
  const { emails, objectStorage, withBlockedServer } = options

  let objectStorageConfig: any = {}
  if (objectStorage) {
    await objectStorage.prepareDefaultMockBuckets()

    objectStorageConfig = objectStorage.getDefaultMockConfig()
  }

  const emailPort = await MockSmtpServer.Instance.collectEmails(emails)

  const overrideConfig = {
    ...objectStorageConfig,
    ...ConfigCommand.getEmailOverrideConfig(emailPort),
    ...ConfigCommand.getDisableRatesLimitOverrideConfig()
  }

  const [ server, remoteServer, blockedServer ] = await Promise.all([
    createSingleServer(1, overrideConfig),
    createSingleServer(2, overrideConfig),

    withBlockedServer
      ? createSingleServer(3)
      : Promise.resolve(undefined)
  ])

  const servers = [ server, remoteServer, blockedServer ].filter(s => !!s)

  await setAccessTokensToServers(servers)
  await setDefaultVideoChannel(servers)

  await remoteServer.config.enableMinimumTranscoding()

  await Promise.all([
    doubleFollow(server, remoteServer),

    withBlockedServer
      ? doubleFollow(server, blockedServer)
      : Promise.resolve(undefined),

    withBlockedServer
      ? doubleFollow(remoteServer, blockedServer)
      : Promise.resolve(undefined)
  ])

  const mouskaToken = await server.users.generateUserAndToken('mouska')
  const noahToken = await server.users.generateUserAndToken('noah')
  const remoteNoahToken = await remoteServer.users.generateUserAndToken('noah_remote')

  // Channel
  const { id: noahSecondChannelId } = await server.channels.create({
    token: noahToken,
    attributes: {
      name: 'noah_second_channel',
      displayName: 'noah display name',
      description: 'noah description',
      support: 'noah support'
    }
  })

  await server.channels.updateImage({
    channelName: 'noah_second_channel',
    fixture: 'banner.jpg',
    type: 'banner'
  })

  await server.channels.updateImage({
    channelName: 'noah_second_channel',
    fixture: 'avatar.png',
    type: 'avatar'
  })

  // Videos
  const externalVideo = await remoteServer.videos.quickUpload({ name: 'external video', privacy: VideoPrivacy.PUBLIC })

  // eslint-disable-next-line max-len
  const noahPrivateVideo = await server.videos.quickUpload({ name: 'noah private video', token: noahToken, privacy: VideoPrivacy.PRIVATE })
  const noahVideo = await server.videos.quickUpload({ name: 'noah public video', token: noahToken, privacy: VideoPrivacy.PUBLIC })
  // eslint-disable-next-line max-len
  const noahVideo2 = await server.videos.upload({
    token: noahToken,
    attributes: {
      fixture: 'video_short.webm',
      name: 'noah public video second channel',
      category: 12,
      tags: [ 'tag1', 'tag2' ],
      commentsPolicy: VideoCommentPolicy.DISABLED,
      description: 'video description',
      downloadEnabled: false,
      language: 'fr',
      licence: 1,
      nsfw: false,
      originallyPublishedAt: new Date(0).toISOString(),
      support: 'video support',
      waitTranscoding: true,
      channelId: noahSecondChannelId,
      privacy: VideoPrivacy.PUBLIC,
      thumbnailfile: 'custom-thumbnail.jpg',
      previewfile: 'custom-preview.jpg'
    }
  })

  await server.videos.quickUpload({ name: 'mouska private video', token: mouskaToken, privacy: VideoPrivacy.PRIVATE })
  const mouskaVideo = await server.videos.quickUpload({ name: 'mouska public video', token: mouskaToken, privacy: VideoPrivacy.PUBLIC })

  // Captions
  await server.captions.add({ language: 'ar', videoId: noahVideo.uuid, fixture: 'subtitle-good1.vtt' })
  await server.captions.add({ language: 'fr', videoId: noahVideo.uuid, fixture: 'subtitle-good1.vtt' })

  await server.captions.add({ language: 'zh', videoId: noahVideo2.uuid, fixture: 'subtitle-good1.vtt' })
  await server.captions.add({ language: 'es', videoId: noahVideo2.uuid, fixture: 'subtitle-good1.vtt' })

  // Chapters
  await server.chapters.update({
    videoId: noahVideo.uuid,
    chapters: [
      { timecode: 1, title: 'chapter 1' },
      { timecode: 3, title: 'chapter 2' }
    ]
  })

  // My settings
  await server.users.updateMe({ token: noahToken, description: 'super noah description', p2pEnabled: false, language: 'fr' })

  // My notification settings
  await server.notifications.updateMySettings({
    token: noahToken,

    settings: {
      ...getAllNotificationsSettings(),

      myVideoPublished: UserNotificationSettingValue.NONE,
      commentMention: UserNotificationSettingValue.EMAIL
    }
  })

  // Rate
  await waitJobs([ server, remoteServer ])

  await server.videos.rate({ id: mouskaVideo.uuid, token: noahToken, rating: 'like' })
  await server.videos.rate({ id: noahVideo.uuid, token: noahToken, rating: 'like' })
  await server.videos.rate({ id: externalVideo.uuid, token: noahToken, rating: 'dislike' })

  await server.videos.rate({ id: noahVideo.uuid, token: mouskaToken, rating: 'like' })

  // 2 followers
  await remoteServer.subscriptions.add({ targetUri: 'noah_channel@' + server.host })
  await server.subscriptions.add({ targetUri: 'noah_channel@' + server.host })

  // 2 following
  await server.subscriptions.add({ token: noahToken, targetUri: 'mouska_channel@' + server.host })
  await server.subscriptions.add({ token: noahToken, targetUri: 'root_channel@' + remoteServer.host })

  // 2 playlists
  await server.playlists.quickCreate({ displayName: 'root playlist' })
  const noahPlaylist = await server.playlists.quickCreate({ displayName: 'noah playlist 1', token: noahToken })
  await server.playlists.quickCreate({ displayName: 'noah playlist 2', token: noahToken, privacy: VideoPlaylistPrivacy.PRIVATE })

  // eslint-disable-next-line max-len
  await server.playlists.addElement({
    playlistId: noahPlaylist.uuid,
    token: noahToken,
    attributes: { videoId: mouskaVideo.uuid, startTimestamp: 2, stopTimestamp: 3 }
  })
  await server.playlists.addElement({ playlistId: noahPlaylist.uuid, token: noahToken, attributes: { videoId: noahVideo.uuid } })
  await server.playlists.addElement({ playlistId: noahPlaylist.uuid, token: noahToken, attributes: { videoId: noahPrivateVideo.uuid } })

  // 3 threads and some replies
  await remoteServer.comments.createThread({ videoId: noahVideo.uuid, text: 'remote comment' })
  await waitJobs([ server, remoteServer ])

  await server.comments.createThread({ videoId: noahVideo.uuid, text: 'local comment' })
  await server.comments.addReplyToLastThread({ token: noahToken, text: 'noah reply' })

  await server.comments.createThread({ videoId: mouskaVideo.uuid, token: noahToken, text: 'noah comment' })

  // Fetch user ids
  const rootId = (await server.users.getMyInfo()).id
  const noahId = (await server.users.getMyInfo({ token: noahToken })).id
  const remoteRootId = (await remoteServer.users.getMyInfo()).id
  const remoteNoahId = (await remoteServer.users.getMyInfo({ token: remoteNoahToken })).id

  // Lives
  await server.config.enableMinimumTranscoding()
  await server.config.enableLive({ allowReplay: true })

  const noahLive = await server.live.create({
    fields: {
      permanentLive: true,
      saveReplay: true,
      latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
      replaySettings: {
        privacy: VideoPrivacy.PUBLIC
      },
      videoPasswords: [ 'password1' ],
      channelId: noahSecondChannelId,
      name: 'noah live video',
      privacy: VideoPrivacy.PASSWORD_PROTECTED,
      schedules: [
        { startAt: new Date(Date.now() + 1000 * 60 * 60).toISOString() }
      ]
    },
    token: noahToken
  })

  // Views
  await server.views.view({ id: noahVideo.uuid, token: noahToken, currentTime: 4 })
  await server.views.view({ id: externalVideo.uuid, token: noahToken, currentTime: 2 })

  // Watched words and auto tag policies
  await servers[0].watchedWordsLists.createList({
    token: noahToken,
    listName: 'forbidden-list',
    words: [ 'forbidden' ],
    accountName: 'noah'
  })

  await servers[0].watchedWordsLists.createList({
    token: noahToken,
    listName: 'allowed-list',
    words: [ 'allowed', 'allowed2' ],
    accountName: 'noah'
  })

  await servers[0].autoTags.updateCommentPolicies({
    accountName: 'noah',
    review: [ 'external-link', 'forbidden-list' ],
    token: noahToken
  })

  return {
    rootId,

    mouskaToken,
    mouskaVideo,

    remoteRootId,
    remoteNoahId,
    remoteNoahToken,

    externalVideo,

    noahId,
    noahToken,
    noahPlaylist,
    noahPrivateVideo,
    noahVideo,
    noahLive,

    server,
    remoteServer,
    blockedServer
  }
}
