import { describe, expect, it } from 'vitest'
import { createHash } from 'node:crypto'
import { gzipSync } from 'node:zlib'
import {
  detectFrpcPlatform,
  FRPC_PLATFORMS,
  FRPC_RELEASES,
  provisionFrpc,
  type FrpcPlatform,
  type FrpcReleaseRef,
  type ProvisionFrpcDeps,
} from '../src/provision/frpcBinary.js'
import {
  extractTarFileByBasename,
  extractFrpcBinary,
  TarExtractError,
} from '../src/provision/untar.js'

function sha256Hex(data: Uint8Array): string {
  return createHash('sha256').update(data).digest('hex')
}

// ---------------------------------------------------------------------------
// In-test tar/gzip fixture builder — hand-builds real USTAR blocks so the
// extractor is exercised against the SAME on-disk layout frp ships (dir entry
// + regular files), with zero network access.
// ---------------------------------------------------------------------------
const TAR_BLOCK = 512
const TYPE_FILE = '0'
const TYPE_DIR = '5'

interface TarEntrySpec {
  readonly name: string
  readonly content: Uint8Array
  readonly typeflag?: string
}

function writeOctalField(block: Uint8Array, offset: number, len: number, value: number): void {
  const s = value.toString(8).padStart(len - 1, '0')
  block.set(new TextEncoder().encode(s), offset)
  block[offset + len - 1] = 0 // NUL terminator
}

function tarHeader(name: string, size: number, typeflag: string): Uint8Array {
  const block = new Uint8Array(TAR_BLOCK)
  const enc = new TextEncoder()
  block.set(enc.encode(name).subarray(0, 100), 0)
  writeOctalField(block, 100, 8, 0o755) // mode
  writeOctalField(block, 108, 8, 0) // uid
  writeOctalField(block, 116, 8, 0) // gid
  writeOctalField(block, 124, 12, size) // size
  writeOctalField(block, 136, 12, 0) // mtime
  block[156] = typeflag.charCodeAt(0)
  block.set(enc.encode('ustar'), 257) // magic "ustar\0"
  block[263] = 0x30 // version "00"
  block[264] = 0x30
  // checksum: fields spaces during compute, then written as 6 octal + NUL + space
  for (let i = 148; i < 156; i++) block[i] = 0x20
  let sum = 0
  for (let i = 0; i < TAR_BLOCK; i++) sum += block[i] ?? 0
  block.set(enc.encode(sum.toString(8).padStart(6, '0')), 148)
  block[154] = 0
  block[155] = 0x20
  return block
}

function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
  const total = parts.reduce((n, p) => n + p.length, 0)
  const out = new Uint8Array(total)
  let off = 0
  for (const p of parts) {
    out.set(p, off)
    off += p.length
  }
  return out
}

function makeTar(entries: readonly TarEntrySpec[]): Uint8Array {
  const parts: Uint8Array[] = []
  for (const e of entries) {
    parts.push(tarHeader(e.name, e.content.length, e.typeflag ?? TYPE_FILE))
    parts.push(e.content)
    const pad = (TAR_BLOCK - (e.content.length % TAR_BLOCK)) % TAR_BLOCK
    if (pad > 0) parts.push(new Uint8Array(pad))
  }
  parts.push(new Uint8Array(TAR_BLOCK * 2)) // end-of-archive: two zero blocks
  return concatBytes(parts)
}

function makeTarGz(entries: readonly TarEntrySpec[]): Uint8Array {
  return new Uint8Array(gzipSync(makeTar(entries)))
}

const FRPC_BYTES = new Uint8Array([0x7f, 0x45, 0x4c, 0x46, 0xde, 0xad, 0xbe, 0xef]) // fake "ELF" frpc
const FRPS_BYTES = new Uint8Array([0x7f, 0x45, 0x4c, 0x46, 0x00, 0x11, 0x22, 0x33]) // decoy frps

/** A realistic frp archive layout: dir entry + frpc + decoy frps + LICENSE. */
function realisticFrpTarGz(prefix = 'frp_0.61.1_darwin_arm64'): Uint8Array {
  return makeTarGz([
    { name: `${prefix}/`, content: new Uint8Array(0), typeflag: TYPE_DIR },
    { name: `${prefix}/frps`, content: FRPS_BYTES },
    { name: `${prefix}/frpc`, content: FRPC_BYTES },
    { name: `${prefix}/LICENSE`, content: new TextEncoder().encode('MIT') },
  ])
}

interface FakeFsState {
  writes: Map<string, Uint8Array>
  renames: Array<{ from: string; to: string }>
  removed: string[]
  chmods: Array<{ path: string; mode: number }>
  mkdirs: string[]
}

function makeFakeDeps(bytesByUrl: Record<string, Uint8Array>): {
  deps: ProvisionFrpcDeps
  state: FakeFsState
  fetchedUrls: string[]
} {
  const state: FakeFsState = {
    writes: new Map(),
    renames: [],
    removed: [],
    chmods: [],
    mkdirs: [],
  }
  const fetchedUrls: string[] = []
  const deps: ProvisionFrpcDeps = {
    fetch: async (url) => {
      fetchedUrls.push(url)
      const bytes = bytesByUrl[url]
      if (!bytes) throw new Error(`test: no fixture bytes for ${url}`)
      return bytes
    },
    fs: {
      mkdir: async (dir) => {
        state.mkdirs.push(dir)
      },
      writeFile: async (path, data) => {
        state.writes.set(path, data)
      },
      rename: async (from, to) => {
        state.renames.push({ from, to })
        const data = state.writes.get(from)
        if (data) {
          state.writes.delete(from)
          state.writes.set(to, data)
        }
      },
      chmod: async (path, mode) => {
        state.chmods.push({ path, mode })
      },
      rm: async (path) => {
        state.removed.push(path)
        state.writes.delete(path)
      },
    },
  }
  return { deps, state, fetchedUrls }
}

function releaseOverride(
  platform: FrpcPlatform,
  ref: FrpcReleaseRef,
): Record<FrpcPlatform, FrpcReleaseRef> {
  return { ...FRPC_RELEASES, [platform]: ref }
}

const BIN_DIR = '/opt/wt/bin'
const BIN_PATH = '/opt/wt/bin/frpc'

describe('detectFrpcPlatform (B3)', () => {
  it('maps darwin/linux × arm64/amd64 (node x64 → amd64)', () => {
    expect(detectFrpcPlatform('darwin', 'arm64')).toBe('darwin-arm64')
    expect(detectFrpcPlatform('darwin', 'x64')).toBe('darwin-amd64')
    expect(detectFrpcPlatform('linux', 'arm64')).toBe('linux-arm64')
    expect(detectFrpcPlatform('linux', 'x64')).toBe('linux-amd64')
  })

  it('returns null for unsupported platform/arch', () => {
    expect(detectFrpcPlatform('win32', 'x64')).toBeNull()
    expect(detectFrpcPlatform('linux', 'ia32')).toBeNull()
    expect(detectFrpcPlatform('freebsd', 'arm64')).toBeNull()
    expect(detectFrpcPlatform('darwin', 'mips')).toBeNull()
  })
})

describe('FRPC_RELEASES pinning', () => {
  it('pins a release per supported platform (https url, semver, 64-hex sha256)', () => {
    expect([...FRPC_PLATFORMS].sort()).toEqual([
      'darwin-amd64',
      'darwin-arm64',
      'linux-amd64',
      'linux-arm64',
    ])
    for (const platform of FRPC_PLATFORMS) {
      const ref = FRPC_RELEASES[platform]
      expect(ref.url.startsWith('https://')).toBe(true)
      expect(ref.url.endsWith('.tar.gz')).toBe(true)
      expect(ref.version).toMatch(/^\d+\.\d+\.\d+$/)
      expect(ref.sha256).toMatch(/^[0-9a-f]{64}$/)
    }
  })

  it('pins REAL (non-placeholder) frp v0.61.1 checksums', () => {
    for (const platform of FRPC_PLATFORMS) {
      expect(FRPC_RELEASES[platform].sha256).not.toBe('0'.repeat(64))
    }
  })
})

describe('extractTarFileByBasename (tar path-traversal safe extractor)', () => {
  it('returns the bytes of the first regular file whose basename matches', () => {
    const tar = makeTar([
      { name: 'frp_x/', content: new Uint8Array(0), typeflag: TYPE_DIR },
      { name: 'frp_x/frps', content: FRPS_BYTES },
      { name: 'frp_x/frpc', content: FRPC_BYTES },
    ])
    expect(extractTarFileByBasename(tar, 'frpc')).toEqual(FRPC_BYTES)
    expect(extractTarFileByBasename(tar, 'frps')).toEqual(FRPS_BYTES)
  })

  it('does NOT match a directory entry that shares the basename', () => {
    const tar = makeTar([
      { name: 'frpc/', content: new Uint8Array(0), typeflag: TYPE_DIR },
      { name: 'frp_x/frpc', content: FRPC_BYTES },
    ])
    expect(extractTarFileByBasename(tar, 'frpc')).toEqual(FRPC_BYTES)
  })

  it('throws when no matching file entry exists', () => {
    const tar = makeTar([{ name: 'frp_x/frps', content: FRPS_BYTES }])
    expect(() => extractTarFileByBasename(tar, 'frpc')).toThrow(TarExtractError)
  })

  it('REJECTS a matching entry whose name contains a ".." traversal segment', () => {
    const tar = makeTar([{ name: 'frp_x/../../../tmp/frpc', content: FRPC_BYTES }])
    expect(() => extractTarFileByBasename(tar, 'frpc')).toThrow(/unsafe|traversal|\.\./i)
  })

  it('REJECTS a matching entry with an absolute path name', () => {
    const tar = makeTar([{ name: '/etc/frpc', content: FRPC_BYTES }])
    expect(() => extractTarFileByBasename(tar, 'frpc')).toThrow(/unsafe|absolute/i)
  })

  it('throws on a corrupt (truncated) tar rather than reading out of bounds', () => {
    const full = makeTar([{ name: 'frp_x/frpc', content: FRPC_BYTES }])
    const truncated = full.subarray(0, TAR_BLOCK + 4) // header + partial content
    expect(() => extractTarFileByBasename(truncated, 'frpc')).toThrow(TarExtractError)
  })
})

describe('extractFrpcBinary (gunzip + extract)', () => {
  it('gunzips then extracts the inner frpc bytes', () => {
    expect(extractFrpcBinary(realisticFrpTarGz())).toEqual(FRPC_BYTES)
  })

  it('throws on non-gzip input', () => {
    expect(() => extractFrpcBinary(new Uint8Array([1, 2, 3, 4]))).toThrow(TarExtractError)
  })
})

describe('provisionFrpc (B3 verify-download + extract discipline)', () => {
  it('selects the arch URL, VERIFIES the archive, and places the INNER frpc (not the archive)', async () => {
    const archive = realisticFrpTarGz()
    const url = 'https://example.test/frp-darwin-arm64.tar.gz'
    const releases = releaseOverride('darwin-arm64', {
      version: '0.61.1',
      url,
      sha256: sha256Hex(archive),
    })
    const { deps, state, fetchedUrls } = makeFakeDeps({ [url]: archive })

    const result = await provisionFrpc(
      { platform: 'darwin', arch: 'arm64', binDir: BIN_DIR, releases },
      deps,
    )

    expect(fetchedUrls).toEqual([url])
    expect(result.binPath).toBe(BIN_PATH)
    expect(result.version).toBe('0.61.1')
    expect(result.platform).toBe('darwin-arm64')
    // atomic place: renamed into the final path, made executable
    expect(state.renames.some((r) => r.to === BIN_PATH)).toBe(true)
    expect(state.chmods.some((c) => (c.mode & 0o111) !== 0)).toBe(true)
    // the placed file is the EXTRACTED frpc binary, NOT the .tar.gz archive
    const placed = state.writes.get(BIN_PATH)
    expect(placed).toEqual(FRPC_BYTES)
    expect(placed).not.toEqual(archive)
  })

  it('ignores the decoy frps entry and places frpc even when frps precedes it', async () => {
    const archive = makeTarGz([
      { name: 'frp_x/frps', content: FRPS_BYTES },
      { name: 'frp_x/frpc', content: FRPC_BYTES },
    ])
    const url = 'https://example.test/frp.tar.gz'
    const releases = releaseOverride('linux-amd64', {
      version: '0.61.1',
      url,
      sha256: sha256Hex(archive),
    })
    const { deps, state } = makeFakeDeps({ [url]: archive })

    await provisionFrpc({ platform: 'linux', arch: 'x64', binDir: BIN_DIR, releases }, deps)

    expect(state.writes.get(BIN_PATH)).toEqual(FRPC_BYTES)
  })

  it('REJECTS on sha256 mismatch and places NO binary (temp cleaned up, no extraction)', async () => {
    const archive = realisticFrpTarGz()
    const url = 'https://example.test/frp-linux-amd64.tar.gz'
    const releases = releaseOverride('linux-amd64', {
      version: '0.61.1',
      url,
      sha256: 'f'.repeat(64), // deliberately wrong
    })
    const { deps, state } = makeFakeDeps({ [url]: archive })

    await expect(
      provisionFrpc({ platform: 'linux', arch: 'x64', binDir: BIN_DIR, releases }, deps),
    ).rejects.toThrow(/sha-?256|hash|integrity|mismatch/i)

    expect(state.renames).toEqual([])
    expect(state.writes.has(BIN_PATH)).toBe(false)
    expect(state.removed.length).toBeGreaterThan(0) // unverified temp removed
  })

  it('never places or execs before verifying (mismatch leaves nothing executable)', async () => {
    const archive = realisticFrpTarGz()
    const url = 'https://example.test/bad.tar.gz'
    const releases = releaseOverride('linux-arm64', {
      version: '0.61.1',
      url,
      sha256: '0'.repeat(64),
    })
    const { deps, state } = makeFakeDeps({ [url]: archive })

    await expect(
      provisionFrpc({ platform: 'linux', arch: 'arm64', binDir: BIN_DIR, releases }, deps),
    ).rejects.toThrow()

    expect(state.chmods.every((c) => c.path !== BIN_PATH)).toBe(true)
  })

  it('throws and places nothing when the verified archive has NO frpc entry', async () => {
    const archive = makeTarGz([
      { name: 'frp_x/frps', content: FRPS_BYTES },
      { name: 'frp_x/LICENSE', content: new TextEncoder().encode('MIT') },
    ])
    const url = 'https://example.test/no-frpc.tar.gz'
    const releases = releaseOverride('darwin-amd64', {
      version: '0.61.1',
      url,
      sha256: sha256Hex(archive),
    })
    const { deps, state } = makeFakeDeps({ [url]: archive })

    await expect(
      provisionFrpc({ platform: 'darwin', arch: 'x64', binDir: BIN_DIR, releases }, deps),
    ).rejects.toThrow(/extract|frpc|tar/i)

    expect(state.renames).toEqual([])
    expect(state.writes.has(BIN_PATH)).toBe(false)
    expect(state.removed.length).toBeGreaterThan(0) // temp removed on extraction failure
  })

  it('REJECTS a traversal frpc entry and never writes outside binDir', async () => {
    const archive = makeTarGz([
      { name: 'frp_x/../../../tmp/frpc', content: FRPC_BYTES },
    ])
    const url = 'https://example.test/evil.tar.gz'
    const releases = releaseOverride('linux-amd64', {
      version: '0.61.1',
      url,
      sha256: sha256Hex(archive),
    })
    const { deps, state } = makeFakeDeps({ [url]: archive })

    await expect(
      provisionFrpc({ platform: 'linux', arch: 'x64', binDir: BIN_DIR, releases }, deps),
    ).rejects.toThrow()

    // nothing placed, and every write that ever happened stayed inside binDir
    expect(state.writes.has(BIN_PATH)).toBe(false)
    expect(state.renames).toEqual([])
    expect([...state.writes.keys()].every((p) => p.startsWith(`${BIN_DIR}/`))).toBe(true)
    expect(state.removed.every((p) => p.startsWith(`${BIN_DIR}/`))).toBe(true)
  })

  it('REJECTS a hash-matching but corrupt (non-gzip) archive after the gate', async () => {
    const garbage = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]) // hashes fine, not a gzip
    const url = 'https://example.test/corrupt.tar.gz'
    const releases = releaseOverride('darwin-arm64', {
      version: '0.61.1',
      url,
      sha256: sha256Hex(garbage),
    })
    const { deps, state } = makeFakeDeps({ [url]: garbage })

    await expect(
      provisionFrpc({ platform: 'darwin', arch: 'arm64', binDir: BIN_DIR, releases }, deps),
    ).rejects.toThrow(/extract|gzip|tar/i)

    expect(state.writes.has(BIN_PATH)).toBe(false)
    expect(state.removed.length).toBeGreaterThan(0)
  })

  it('throws a clear error on an unsupported platform (nothing fetched)', async () => {
    const { deps, fetchedUrls } = makeFakeDeps({})
    await expect(
      provisionFrpc({ platform: 'win32', arch: 'x64', binDir: BIN_DIR }, deps),
    ).rejects.toThrow(/unsupported|platform/i)
    expect(fetchedUrls).toEqual([])
  })
})
