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 { 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[]
  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
  writeManifest: (
    directory: string,
    options: ReleaseOptions
  ) => Promise<{
    files: Array<{
      name: string
      size: number
      sha256: string
    }>
  }>
}

const require = createRequire(import.meta.url)
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
}

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',
        'portable',
        '--arm64',
        '--config.directories.output=C:\\release-stage',
        expect.stringContaining('nsis.artifactName='),
        expect.stringContaining('portable.artifactName=')
      ])
    )
  })
})

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('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', () => {
    const directory = mkdtempSync(
      join(tmpdir(), 'goodbuddy-release-artifacts-')
    )
    try {
      writeFileSync(
        join(
          directory,
          'GoodBuddy-0.1.0-windows-x64-setup.exe'
        ),
        'MZ'
      )
      writeFileSync(
        join(
          directory,
          'GoodBuddy-0.1.0-windows-x64-portable.exe'
        ),
        'MZ'
      )
      expect(() =>
        releaseBuilder.verifyArtifacts(
          directory,
          windowsOptions
        )
      ).not.toThrow()
      rmSync(
        join(
          directory,
          'GoodBuddy-0.1.0-windows-x64-portable.exe'
        )
      )
      expect(() =>
        releaseBuilder.verifyArtifacts(
          directory,
          windowsOptions
        )
      ).toThrow('portable 产物数量错误')
    } finally {
      rmSync(directory, { recursive: true, force: true })
    }
  })
})
