import { createHash } from 'node:crypto'
import {
  chmod,
  mkdir,
  mkdtemp,
  readFile,
  rm,
  symlink,
  writeFile,
} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { stageServerApp } from '../../scripts/stage-server-app.mjs'
import { verifyServerPackage } from '../../scripts/verify-server-package.mjs'

const temporaryRoots: string[] = []

interface FixtureEngineLock {
  engine: string
  version: string
  assets: Record<string, { bin: string; binarySha256: string }>
}

afterEach(async () => {
  await Promise.all(
    temporaryRoots.splice(0).map((root) => rm(root, { recursive: true }))
  )
})

async function writeFixtureFile(
  root: string,
  relativePath: string,
  content: string | Buffer = relativePath
): Promise<void> {
  const target = path.join(root, relativePath)
  await mkdir(path.dirname(target), { recursive: true })
  await writeFile(target, content)
}

function nativeHeader(platform: string, arch: string): Buffer {
  if (platform === 'win32') {
    const header = Buffer.alloc(72)
    header.write('MZ')
    header.writeUInt32LE(64, 0x3c)
    header.set([0x50, 0x45, 0, 0], 64)
    header.writeUInt16LE(arch === 'arm64' ? 0xaa64 : 0x8664, 68)
    return header
  }
  if (platform === 'linux') {
    const header = Buffer.alloc(20)
    header.set([0x7f, 0x45, 0x4c, 0x46])
    header[4] = 2
    header[5] = 1
    header.writeUInt16LE(arch === 'arm64' ? 183 : 62, 18)
    return header
  }
  const header = Buffer.alloc(8)
  header.set([0xcf, 0xfa, 0xed, 0xfe])
  header.writeUInt32LE(arch === 'arm64' ? 0x0100000c : 0x01000007, 4)
  return header
}

function fixtureContract() {
  return {
    schemaVersion: 1,
    supportedTargets: [
      'darwin-arm64',
      'darwin-x64',
      'linux-arm64-gnu',
      'linux-arm64-musl',
      'linux-x64-gnu',
      'linux-x64-musl',
      'win32-x64',
    ],
    runtimeRoots: ['better-sqlite3'],
    buildInputs: [
      {
        source: 'dist/core/plugin/host/quick-js-worker.cjs',
        destination: 'dist/core/plugin/host/quick-js-worker.cjs',
        type: 'file',
        entry: null,
        scanExternals: true,
      },
      {
        source: 'dist/renderer-web',
        destination: 'dist/renderer-web',
        type: 'directory',
        entry: 'index.html',
        scanExternals: false,
      },
      {
        source: 'dist/server',
        destination: 'dist/server',
        type: 'directory',
        entry: ['index.mjs', 'motrix-admin.mjs'],
        scanExternals: true,
      },
    ],
    resourceInputs: [
      {
        source: 'LICENSE',
        destination: 'LICENSE',
        type: 'file',
      },
      {
        source: 'legal/notice.txt',
        destination: 'legal/notice.txt',
        type: 'file',
      },
    ],
  }
}

function fixtureBudgets(overrides: Record<string, number> = {}) {
  return {
    schemaVersion: 1,
    artifactBytes: 1_000_000,
    dependencyBytes: 1_000_000,
    packageInstances: 10,
    unexpectedRuntimeRoots: 0,
    unresolvedExternals: 0,
    foreignNativeBinaries: 0,
    betterSqlite3Prebuilds: 1,
    ...overrides,
  }
}

async function createStagedFixture(
  options: { withEngine?: boolean } = {}
): Promise<{
  root: string
  stageRoot: string
  reportPath: string
  contract: ReturnType<typeof fixtureContract>
  engineLock?: FixtureEngineLock
}> {
  const root = await mkdtemp(path.join(os.tmpdir(), 'motrix-server-verify-'))
  temporaryRoots.push(root)
  await writeFixtureFile(
    root,
    'package.json',
    `${JSON.stringify(
      {
        name: 'motrix-server-fixture',
        version: '1.0.0',
        license: 'MIT',
        dependencies: { 'better-sqlite3': '13.0.3' },
      },
      null,
      2
    )}\n`
  )
  await writeFixtureFile(root, 'LICENSE', 'MIT License\n')
  await writeFixtureFile(
    root,
    'dist/server/index.mjs',
    'import Database from "better-sqlite3"; export default Database\n'
  )
  await writeFixtureFile(
    root,
    'dist/server/motrix-admin.mjs',
    'import path from "node:path"; export default path.sep;\n'
  )
  await writeFixtureFile(
    root,
    'dist/core/plugin/host/quick-js-worker.cjs',
    'module.exports = require("node:path")\n'
  )
  await writeFixtureFile(
    root,
    'dist/renderer-web/index.html',
    '<main>ok</main>'
  )
  await writeFixtureFile(root, 'legal/notice.txt', 'legal')
  await writeFixtureFile(
    root,
    'node_modules/better-sqlite3/package.json',
    `${JSON.stringify(
      {
        name: 'better-sqlite3',
        version: '13.0.3',
        main: 'lib/index.js',
      },
      null,
      2
    )}\n`
  )
  await writeFixtureFile(
    root,
    'node_modules/better-sqlite3/lib/index.js',
    'module.exports = true\n'
  )
  await writeFixtureFile(root, 'node_modules/better-sqlite3/LICENSE', 'MIT')
  for (const [name, platform, arch] of [
    ['darwin-arm64.node', 'darwin', 'arm64'],
    ['darwin-x64.node', 'darwin', 'x64'],
    ['linux-arm64.node', 'linux', 'arm64'],
    ['linux-x64.node', 'linux', 'x64'],
    ['linuxmusl-arm64.node', 'linux', 'arm64'],
    ['linuxmusl-x64.node', 'linux', 'x64'],
    ['win32-x64.node', 'win32', 'x64'],
  ]) {
    await writeFixtureFile(
      root,
      `node_modules/better-sqlite3/prebuilds/${name}`,
      nativeHeader(platform, arch)
    )
  }
  const contract = fixtureContract()
  let engineLock: FixtureEngineLock | undefined
  if (options.withEngine) {
    const engineBytes = nativeHeader('darwin', 'arm64')
    await writeFixtureFile(root, 'extra/darwin/arm64/aria2c', engineBytes)
    await chmod(path.join(root, 'extra/darwin/arm64/aria2c'), 0o755)
    contract.resourceInputs.splice(1, 0, {
      source: 'extra/{platform}/{arch}/{aria2Binary}',
      destination: 'bin/{aria2Binary}',
      type: 'file',
    })
    engineLock = {
      engine: 'aria2',
      version: '1.37.0-motrix.test',
      assets: {
        'darwin-arm64': {
          bin: 'aria2c',
          binarySha256: createHash('sha256').update(engineBytes).digest('hex'),
        },
      },
    }
  }
  await stageServerApp({
    repoRoot: root,
    platform: 'darwin',
    arch: 'arm64',
    strict: true,
    contract,
    budgets: fixtureBudgets(),
  })
  return {
    root,
    stageRoot: path.join(root, 'dist/server-app'),
    reportPath: path.join(root, 'reports/server-darwin-arm64.json'),
    contract,
    engineLock,
  }
}

async function verifyFixture(
  fixture: Awaited<ReturnType<typeof createStagedFixture>>,
  budgets = fixtureBudgets()
) {
  return verifyServerPackage({
    appDir: fixture.stageRoot,
    platform: 'darwin',
    arch: 'arm64',
    contract: fixture.contract,
    budgets,
    reportPath: fixture.reportPath,
    engineLock: fixture.engineLock,
  })
}

describe('verifyServerPackage', () => {
  it('accepts an intact staged Server artifact and emits a sanitized report', async () => {
    const fixture = await createStagedFixture()
    const report = await verifyFixture(fixture)

    expect(report.passed).toBe(true)
    expect(report.metrics.packageInstances).toBe(1)
    expect(report.metrics.betterSqlite3Prebuilds).toBe(1)
    expect(report.metrics.foreignNativeBinaries).toBe(0)
    const reportJson = await readFile(fixture.reportPath, 'utf8')
    expect(reportJson).not.toContain(fixture.root)
    expect(JSON.parse(reportJson)).toEqual(report)
  })

  it('accepts only the target aria2 binary pinned by the engine lock', async () => {
    const fixture = await createStagedFixture({ withEngine: true })
    const report = await verifyFixture(fixture)

    expect(report.engineBinary).toEqual(
      expect.objectContaining({
        path: 'bin/aria2c',
        version: '1.37.0-motrix.test',
        format: 'mach-o',
        arch: 'arm64',
      })
    )

    const asset = fixture.engineLock?.assets['darwin-arm64']
    if (!asset) throw new Error('fixture engine lock is missing its asset')
    asset.binarySha256 = '0'.repeat(64)
    await expect(verifyFixture(fixture)).rejects.toThrow('bundled-engine')
  })

  it('fails closed on content drift and still writes the JSON report', async () => {
    const fixture = await createStagedFixture()
    await writeFixtureFile(
      fixture.stageRoot,
      'dist/server/index.mjs',
      'export default "tampered"\n'
    )

    await expect(verifyFixture(fixture)).rejects.toThrow('input-fingerprints')
    const report = JSON.parse(await readFile(fixture.reportPath, 'utf8'))
    expect(report.passed).toBe(false)
    expect(report.checks).toContainEqual(
      expect.objectContaining({ id: 'input-fingerprints', passed: false })
    )
  })

  it('rejects a missing operator CLI bundle', async () => {
    const fixture = await createStagedFixture()
    await rm(path.join(fixture.stageRoot, 'dist/server/motrix-admin.mjs'))

    await expect(verifyFixture(fixture)).rejects.toThrow()
  })

  it('rejects a staged Server artifact without the Motrix license', async () => {
    const fixture = await createStagedFixture()
    await rm(path.join(fixture.stageRoot, 'LICENSE'))

    await expect(verifyFixture(fixture)).rejects.toThrow('project-license')
    const report = JSON.parse(await readFile(fixture.reportPath, 'utf8'))
    expect(report.checks).toContainEqual(
      expect.objectContaining({ id: 'project-license', passed: false })
    )
  })

  it('rejects target drift and foreign native binaries', async () => {
    const fixture = await createStagedFixture()
    const stageManifestPath = path.join(
      fixture.stageRoot,
      '.motrix-server-stage.json'
    )
    const stageManifest = JSON.parse(await readFile(stageManifestPath, 'utf8'))
    stageManifest.target.key = 'darwin-x64'
    await writeFile(stageManifestPath, `${JSON.stringify(stageManifest)}\n`)
    await writeFixtureFile(
      fixture.stageRoot,
      'node_modules/better-sqlite3/prebuilds/win32-x64.node',
      nativeHeader('win32', 'x64')
    )

    await expect(verifyFixture(fixture)).rejects.toThrow('target')
    const report = JSON.parse(await readFile(fixture.reportPath, 'utf8'))
    expect(report.metrics.foreignNativeBinaries).toBe(1)
    expect(report.metrics.betterSqlite3Prebuilds).toBe(2)
  })

  it('rejects manifest roots, package closure drift, and tight budgets', async () => {
    const fixture = await createStagedFixture()
    const appManifestPath = path.join(fixture.stageRoot, 'package.json')
    const appManifest = JSON.parse(await readFile(appManifestPath, 'utf8'))
    appManifest.dependencies.unexpected = '1.0.0'
    await writeFile(appManifestPath, `${JSON.stringify(appManifest)}\n`)
    await writeFixtureFile(
      fixture.stageRoot,
      'node_modules/unexpected/package.json',
      '{"name":"unexpected","version":"1.0.0"}\n'
    )

    await expect(
      verifyFixture(fixture, fixtureBudgets({ artifactBytes: 1 }))
    ).rejects.toThrow('application-manifest')
    const report = JSON.parse(await readFile(fixture.reportPath, 'utf8'))
    expect(report.metrics.unexpectedRuntimeRoots).toBe(1)
    expect(report.checks).toContainEqual(
      expect.objectContaining({ id: 'package-closure', passed: false })
    )
    expect(report.checks).toContainEqual(
      expect.objectContaining({ id: 'size-budgets', passed: false })
    )
  })

  it('rejects symlinks in the staged artifact', async () => {
    const fixture = await createStagedFixture()
    await symlink(
      path.join(fixture.stageRoot, 'legal/notice.txt'),
      path.join(fixture.stageRoot, 'legal/notice-link.txt')
    )

    await expect(verifyFixture(fixture)).rejects.toThrow('symlinks')
    const report = JSON.parse(await readFile(fixture.reportPath, 'utf8'))
    expect(report.passed).toBe(false)
  })
})