/**
 * Tests for the first-run ffmpeg acquisition (ffmpegSetup.ts). The shared streaming +
 * checksum loop is mocked here — it's already pinned by updaterDownload.test.ts — so these
 * focus on the ffmpeg-specific behaviour: the upstream trust gate, readiness + dev seed,
 * the download → tar-extract → install-into-managed-dir flow, and the manual-locate
 * fallback (accept a valid folder, reject a missing/blocked one).
 */
import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest'
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type { WebContents } from 'electron'

const ROOT = mkdtempSync(join(tmpdir(), 'aerofetch-ffmpeg-test-'))
const USERDATA = join(ROOT, 'userData')
const TEMP = join(ROOT, 'temp')
const RES = join(ROOT, 'resources')
const MANAGED = join(USERDATA, 'bin')
// getBinDir() resolves the dev seed under <getAppPath()>/resources/bin (its is.dev branch),
// so mocking is.dev=true + app.getAppPath()=ROOT points it at RES/bin. Deliberately NOT
// mutating the process-global process.resourcesPath, which would leak across test files.

// Shared, per-test-tunable control surface (hoisted so the vi.mock factories can read it).
const h = vi.hoisted(() => ({
  streamOk: true,
  streamError: 'network boom',
  tarFail: false,
  extractWrites: true,
  validateOk: true,
  dialog: { canceled: false, filePaths: [] as string[] }
}))

vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } }))

vi.mock('electron', () => ({
  app: {
    getPath: (k: string) => (k === 'userData' ? join(ROOT, 'userData') : join(ROOT, 'temp')),
    getAppPath: () => ROOT
  },
  dialog: { showOpenDialog: async () => h.dialog },
  BrowserWindow: class {}
}))

vi.mock('../src/main/logger', () => ({
  logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() }
}))

// ffmpeg version read is display-only; stub it so readiness comes purely from file presence.
vi.mock('../src/main/ffmpeg', () => ({
  getFfmpegVersions: async () => ({ ffmpeg: 'mock', ffprobe: 'mock' })
}))

// The streamer is pinned elsewhere: simulate a written archive + one progress tick.
vi.mock('../src/main/lib/verifiedDownload', () => ({
  streamVerifiedFile: async (opts: {
    filePath: string
    onProgress: (p: { received: number; total: number; fraction: number }) => void
  }) => {
    if (!h.streamOk) return { ok: false, error: h.streamError }
    writeFileSync(opts.filePath, 'FAKE-ZIP')
    opts.onProgress({ received: 100, total: 100, fraction: 1 })
    return { ok: true, filePath: opts.filePath }
  }
}))

// tar.exe extract + `<tool> -version` validation both go through execFile.
vi.mock('child_process', () => ({
  execFile: (
    file: string,
    args: string[],
    _opts: unknown,
    cb: (err: Error | null, stdout?: string) => void
  ) => {
    if (file.endsWith('tar.exe')) {
      if (h.tarFail) return cb(new Error('tar failed'))
      const dest = args[args.indexOf('-C') + 1]
      if (h.extractWrites) {
        writeFileSync(join(dest, 'ffmpeg.exe'), 'FF')
        writeFileSync(join(dest, 'ffprobe.exe'), 'FP')
      }
      return cb(null)
    }
    if (args.includes('-version')) {
      return h.validateOk ? cb(null, 'ffmpeg version 8.1.2') : cb(new Error('blocked'))
    }
    return cb(null)
  }
}))

const { isTrustedFfmpegUrl, getFfmpegSetupStatus, downloadFfmpeg, locateFfmpegManually } =
  await import('../src/main/ffmpegSetup')

function fakeWc(): { wc: WebContents; sends: unknown[] } {
  const sends: unknown[] = []
  const wc = {
    isDestroyed: () => false,
    send: (_ch: string, p: unknown) => sends.push(p)
  } as unknown as WebContents
  return { wc, sends }
}

beforeEach(() => {
  rmSync(MANAGED, { recursive: true, force: true })
  rmSync(join(RES, 'bin'), { recursive: true, force: true })
  mkdirSync(TEMP, { recursive: true })
  Object.assign(h, {
    streamOk: true,
    streamError: 'network boom',
    tarFail: false,
    extractWrites: true,
    validateOk: true,
    dialog: { canceled: false, filePaths: [] }
  })
})

afterAll(() => rmSync(ROOT, { recursive: true, force: true }))

describe('isTrustedFfmpegUrl', () => {
  it('accepts HTTPS on an allowlisted upstream host', () => {
    expect(
      isTrustedFfmpegUrl('https://github.com/GyanD/codexffmpeg/releases/download/x/f.zip')
    ).toBe(true)
    expect(isTrustedFfmpegUrl('https://release-assets.githubusercontent.com/abc?x=1')).toBe(true)
  })
  it('rejects http, an off-list host, and garbage', () => {
    expect(isTrustedFfmpegUrl('http://github.com/x.zip')).toBe(false)
    expect(isTrustedFfmpegUrl('https://evil.example/x.zip')).toBe(false)
    expect(isTrustedFfmpegUrl('not a url')).toBe(false)
  })
})

describe('getFfmpegSetupStatus', () => {
  it('is ready only when both binaries are present', async () => {
    mkdirSync(MANAGED, { recursive: true })
    writeFileSync(join(MANAGED, 'ffmpeg.exe'), 'x')
    expect((await getFfmpegSetupStatus()).ready).toBe(false) // ffprobe missing
    writeFileSync(join(MANAGED, 'ffprobe.exe'), 'x')
    expect((await getFfmpegSetupStatus()).ready).toBe(true)
  })

  it('seeds the managed copies from the dev bundle when present', async () => {
    mkdirSync(join(RES, 'bin'), { recursive: true })
    writeFileSync(join(RES, 'bin', 'ffmpeg.exe'), 'seed')
    writeFileSync(join(RES, 'bin', 'ffprobe.exe'), 'seed')
    const status = await getFfmpegSetupStatus()
    expect(status.ready).toBe(true)
    expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(true)
  })
})

describe('downloadFfmpeg', () => {
  it('downloads, extracts, and installs both binaries into the managed dir', async () => {
    const { wc, sends } = fakeWc()
    const r = await downloadFfmpeg(wc)
    expect(r).toEqual({ ok: true })
    expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(true)
    expect(existsSync(join(MANAGED, 'ffprobe.exe'))).toBe(true)
    // A downloading tick and the extracting phase both reach the renderer.
    expect(sends).toContainEqual(expect.objectContaining({ phase: 'downloading' }))
    expect(sends).toContainEqual(expect.objectContaining({ phase: 'extracting' }))
  })

  it('surfaces a download failure and installs nothing', async () => {
    h.streamOk = false
    const { wc } = fakeWc()
    const r = await downloadFfmpeg(wc)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/network boom/)
    expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(false)
  })

  it('fails when extraction fails, leaving the managed dir empty', async () => {
    h.tarFail = true
    const { wc } = fakeWc()
    const r = await downloadFfmpeg(wc)
    expect(r.ok).toBe(false)
    expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(false)
  })

  it('fails when the archive lacks the expected binaries', async () => {
    h.extractWrites = false
    const { wc } = fakeWc()
    const r = await downloadFfmpeg(wc)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/missing/i)
  })
})

describe('locateFfmpegManually', () => {
  function folderWith(files: string[]): string {
    const dir = mkdtempSync(join(ROOT, 'located-'))
    for (const f of files) writeFileSync(join(dir, f), 'x')
    return dir
  }

  it('installs from a folder that holds both runnable binaries', async () => {
    h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe', 'ffprobe.exe'])] }
    const r = await locateFfmpegManually()
    expect(r).toEqual({ ok: true })
    expect(existsSync(join(MANAGED, 'ffprobe.exe'))).toBe(true)
  })

  it('rejects a folder missing a binary', async () => {
    h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe'])] }
    const r = await locateFfmpegManually()
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/doesn't contain/i)
  })

  it('rejects binaries that will not run', async () => {
    h.validateOk = false
    h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe', 'ffprobe.exe'])] }
    const r = await locateFfmpegManually()
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/could not be run/i)
  })

  it('reports a dismissed folder picker without installing anything', async () => {
    h.dialog = { canceled: true, filePaths: [] }
    const r = await locateFfmpegManually()
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/no folder selected/i)
  })
})
