import {
  existsSync,
  mkdtempSync,
  mkdirSync,
  readFileSync,
  rmSync,
  writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Writable } from 'node:stream'
import { describe, expect, it } from 'vitest'

interface ReleaseOptions {
  platform: 'windows' | 'macos' | 'linux'
  arch: 'x64' | 'arm64'
  formats: string[]
  skipBuild: boolean
  dryRun: boolean
  help: boolean
}

interface ReleaseBuilderModule {
  assertReplaceableOutput: (
    directory: string,
    options: ReleaseOptions
  ) => void
  buildElectronBuilderArguments: (
    options: ReleaseOptions,
    outputDirectory: string
  ) => string[]
  createPortableZip: (
    unpackedDirectory: string,
    zipPath: string,
    dependencies?: {
      openOutput: (filePath: string) => Writable
    }
  ) => Promise<void>
  detectBinaryArchitecture: (
    buffer: Buffer
  ) => 'x64' | 'arm64' | undefined
  parseArguments: (
    arguments_: string[],
    environment?: { platform: string; arch: string }
  ) => ReleaseOptions
  replaceOutput: (
    stagingDirectory: string,
    destination: string,
    options: ReleaseOptions
  ) => void
  verifyArtifacts: (
    directory: string,
    options: ReleaseOptions
  ) => void
  verifyPortableZip: (filePath: string) => void
  writeManifest: (
    directory: string,
    options: ReleaseOptions
  ) => Promise<{
    files: Array<{
      name: string
      size: number
      sha256: string
    }>
  }>
}

const require = createRequire(import.meta.url)
const packageVersion = (
  require('../package.json') as { version: string }
).version
const releaseBuilder = require(
  '../build/build-release.cjs'
) as ReleaseBuilderModule
const windowsOptions: ReleaseOptions = {
  platform: 'windows',
  arch: 'x64',
  formats: ['nsis', 'portable'],
  skipBuild: false,
  dryRun: false,
  help: false
}

function pe(machine: number): Buffer {
  const buffer = Buffer.alloc(256)
  buffer.write('MZ', 0, 'ascii')
  buffer.writeUInt32LE(128, 0x3c)
  buffer.write('PE\0\0', 128, 'ascii')
  buffer.writeUInt16LE(machine, 132)
  return buffer
}

function elf(machine: number, bigEndian = false): Buffer {
  const buffer = Buffer.alloc(64)
  buffer.set([0x7f, 0x45, 0x4c, 0x46])
  buffer[5] = bigEndian ? 2 : 1
  if (bigEndian) {
    buffer.writeUInt16BE(machine, 18)
  } else {
    buffer.writeUInt16LE(machine, 18)
  }
  return buffer
}

function machO(cpuType: number): Buffer {
  const buffer = Buffer.alloc(8)
  buffer.writeUInt32LE(0xfeedfacf, 0)
  buffer.writeUInt32LE(cpuType, 4)
  return buffer
}

function portableDirectory(parent: string): string {
  const directory = join(parent, 'portable')
  mkdirSync(
    join(directory, 'resources', 'runtimes', 'opencode'),
    { recursive: true }
  )
  mkdirSync(
    join(directory, 'resources', 'runtimes', 'continue'),
    { recursive: true }
  )
  for (const [path, content] of [
    ['GoodBuddy.exe', 'MZ'],
    ['resources/app.asar', 'asar'],
    ['resources/icon.ico', 'icon'],
    ['resources/tray-icon.png', 'tray'],
    ['resources/runtimes/opencode/opencode.exe', 'MZ'],
    ['resources/runtimes/continue/package.json', '{}']
  ] satisfies Array<[string, string]>) {
    writeFileSync(join(directory, ...path.split('/')), content)
  }
  return directory
}

function endOfCentralDirectory(
  entryCount: number,
  centralSize: number,
  centralOffset = 0
): Buffer {
  const buffer = Buffer.alloc(22)
  buffer.writeUInt32LE(0x06054b50, 0)
  buffer.writeUInt16LE(entryCount, 8)
  buffer.writeUInt16LE(entryCount, 10)
  buffer.writeUInt32LE(centralSize, 12)
  buffer.writeUInt32LE(centralOffset, 16)
  return buffer
}

describe('release build arguments', () => {
  it.each([
    ['win32', 'x64', 'windows', ['nsis', 'portable']],
    ['darwin', 'arm64', 'macos', ['dmg', 'zip']],
    ['linux', 'x64', 'linux', ['AppImage', 'deb']]
  ])(
    'uses %s host defaults',
    (host, arch, platform, formats) => {
      expect(
        releaseBuilder.parseArguments([], {
          platform: host,
          arch
        })
      ).toMatchObject({ platform, arch, formats })
    }
  )

  it('normalizes aliases, formats, and duplicate formats', () => {
    expect(
      releaseBuilder.parseArguments(
        [
          '--platform',
          'mac',
          '--arch',
          'arm64',
          '--format',
          'DMG,zip,dmg',
          '--skip-build',
          '--dry-run'
        ],
        { platform: 'win32', arch: 'x64' }
      )
    ).toEqual({
      platform: 'macos',
      arch: 'arm64',
      formats: ['dmg', 'zip'],
      skipBuild: true,
      dryRun: true,
      help: false
    })
  })

  it.each([
    [['--platform', 'freebsd'], '不支持当前系统'],
    [['--arch', 'ia32'], '不支持的架构'],
    [['--format', 'rpm'], '不支持打包格式'],
    [['--unknown'], '未知参数']
  ])('rejects invalid arguments', (arguments_, message) => {
    expect(() =>
      releaseBuilder.parseArguments(arguments_, {
        platform: 'linux',
        arch: 'x64'
      })
    ).toThrow(message)
  })

  it('builds target-specific electron-builder arguments', () => {
    const arguments_ =
      releaseBuilder.buildElectronBuilderArguments(
        {
          platform: 'windows',
          arch: 'arm64',
          formats: ['nsis', 'portable'],
          skipBuild: false,
          dryRun: false,
          help: false
        },
        'C:\\release-stage'
      )

    expect(arguments_).toEqual(
      expect.arrayContaining([
        '--win',
        'nsis',
        'dir',
        '--arm64',
        '--config.directories.output=C:\\release-stage',
        '--publish',
        'never',
        expect.stringContaining('nsis.artifactName=')
      ])
    )
    expect(arguments_).not.toContain('portable')
    expect(
      arguments_.some((argument) =>
        argument.includes('portable.artifactName=')
      )
    ).toBe(false)
  })
})

describe('release binary architecture detection', () => {
  it.each([
    [pe(0x8664), 'x64'],
    [pe(0xaa64), 'arm64'],
    [elf(62), 'x64'],
    [elf(183, true), 'arm64'],
    [machO(0x01000007), 'x64'],
    [machO(0x0100000c), 'arm64'],
    [Buffer.from('not an executable'), undefined]
  ])('detects binary headers', (buffer, expected) => {
    expect(
      releaseBuilder.detectBinaryArchitecture(buffer)
    ).toBe(expected)
  })
})

describe('release output safety', () => {
  it('preserves an existing portable ZIP when exclusive creation fails', async () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-portable-existing-')
    )
    try {
      const unpacked = portableDirectory(directory)
      const zipPath = join(directory, 'portable.zip')
      writeFileSync(zipPath, 'keep-existing-output')

      await expect(
        releaseBuilder.createPortableZip(unpacked, zipPath)
      ).rejects.toMatchObject({ code: 'EEXIST' })
      expect(readFileSync(zipPath, 'utf8')).toBe(
        'keep-existing-output'
      )
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })

  it('handles portable ZIP output stream failures without hanging', async () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-portable-write-error-')
    )
    try {
      const unpacked = portableDirectory(directory)
      const failure = new Error('simulated ZIP write failure')
      const output = new Writable({
        write(_chunk, _encoding, callback) {
          callback(failure)
        }
      })

      await expect(
        releaseBuilder.createPortableZip(
          unpacked,
          join(directory, 'unused.zip'),
          { openOutput: () => output }
        )
      ).rejects.toThrow('simulated ZIP write failure')
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })

  it.each([
    [
      'an excessive entry count',
      endOfCentralDirectory(50_001, 46)
    ],
    [
      'an oversized central directory',
      endOfCentralDirectory(7, 64 * 1024 * 1024 + 1)
    ]
  ])('rejects %s before reading ZIP central data', (_case, bytes) => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-portable-invalid-')
    )
    try {
      const zipPath = join(directory, 'portable.zip')
      writeFileSync(zipPath, bytes)
      expect(() =>
        releaseBuilder.verifyPortableZip(zipPath)
      ).toThrow('中央目录无效')
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })

  it('writes a deterministic artifact manifest with streaming hashes', async () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-release-manifest-')
    )
    try {
      writeFileSync(join(directory, 'artifact.exe'), 'release')
      const manifest = await releaseBuilder.writeManifest(
        directory,
        windowsOptions
      )

      expect(manifest.files).toEqual([
        {
          name: 'artifact.exe',
          size: 7,
          sha256:
            'a4d451ec23463726f72c43d64c710968f6b602cd653b4de8adee1b556240a829'
        }
      ])
      expect(
        JSON.parse(
          readFileSync(
            join(directory, 'release-manifest.json'),
            'utf8'
          )
        )
      ).toMatchObject({
        productName: 'GoodBuddy',
        platform: 'windows',
        arch: 'x64'
      })
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })

  it('refuses to replace an unrecognized non-empty directory', () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-release-unsafe-')
    )
    try {
      writeFileSync(join(directory, 'user-file.txt'), 'keep')
      expect(() =>
        releaseBuilder.assertReplaceableOutput(
          directory,
          windowsOptions
        )
      ).toThrow('拒绝覆盖未识别的发布目录')
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })

  it('atomically replaces a recognized release directory', () => {
    const parent = mkdtempSync(
      join(tmpdir(), 'goodbuddy-release-replace-')
    )
    const destination = join(parent, 'windows-x64')
    const staging = join(parent, 'staging')
    try {
      mkdirSync(destination)
      mkdirSync(staging)
      writeFileSync(
        join(destination, 'release-manifest.json'),
        JSON.stringify({
          formatVersion: 1,
          productName: 'GoodBuddy',
          platform: 'windows',
          arch: 'x64'
        })
      )
      writeFileSync(join(destination, 'old.exe'), 'old')
      writeFileSync(join(staging, 'new.exe'), 'new')

      releaseBuilder.replaceOutput(
        staging,
        destination,
        windowsOptions
      )

      expect(existsSync(join(destination, 'new.exe'))).toBe(true)
      expect(existsSync(join(destination, 'old.exe'))).toBe(false)
      expect(existsSync(staging)).toBe(false)
    } finally {
      rmSync(parent, { recursive: true, force: true })
    }
  })

  it('requires one artifact for every requested format', async () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-release-artifacts-')
    )
    try {
      writeFileSync(
        join(
          directory,
          `GoodBuddy-${packageVersion}-windows-x64-setup.exe`
        ),
        'MZ'
      )
      const unpacked = portableDirectory(directory)
      const portableZip = join(
        directory,
        `GoodBuddy-${packageVersion}-windows-x64-portable.zip`
      )
      await releaseBuilder.createPortableZip(
        unpacked,
        portableZip
      )
      expect(() =>
        releaseBuilder.verifyArtifacts(
          directory,
          windowsOptions
        )
      ).not.toThrow()
      rmSync(portableZip)
      expect(() =>
        releaseBuilder.verifyArtifacts(
          directory,
          windowsOptions
        )
      ).toThrow('portable 产物数量错误')
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })
})
