// build-zip.mjs — Minimal synthetic ZIP builder for tests.
// Supports STORE method only. Lets tests construct adversarial archives that
// real zip tools refuse to emit (zip-slip names, symlink mode bits, oversized
// uncompressed sizes for bomb tests).

import { crc32 } from 'node:zlib';

const SIG_LFH = 0x04034b50;
const SIG_CD  = 0x02014b50;
const SIG_EOCD = 0x06054b50;

function crc(buf) {
  return crc32(buf) >>> 0;
}

/**
 * Build a ZIP buffer from a list of entries.
 * @param {Array<{ name: string, data: Buffer|string, externalAttr?: number, versionMadeBy?: number, declaredUncompSize?: number, declaredCompSize?: number }>} entries
 * @returns {Buffer}
 */
export function buildZip(entries) {
  const lfhParts = [];
  const cdParts = [];
  let offset = 0;

  for (const entry of entries) {
    const nameBuf = Buffer.from(entry.name, 'utf8');
    const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '', 'utf8');
    const compSize = entry.declaredCompSize ?? data.length;
    const uncompSize = entry.declaredUncompSize ?? data.length;
    const c = crc(data);

    // Local file header (30 bytes)
    const lfh = Buffer.alloc(30);
    lfh.writeUInt32LE(SIG_LFH, 0);
    lfh.writeUInt16LE(20, 4);          // version needed
    lfh.writeUInt16LE(0, 6);           // flags
    lfh.writeUInt16LE(0, 8);           // method = STORE
    lfh.writeUInt16LE(0, 10);          // time
    lfh.writeUInt16LE(0, 12);          // date
    lfh.writeUInt32LE(c, 14);          // crc32
    lfh.writeUInt32LE(compSize, 18);   // compressed size
    lfh.writeUInt32LE(uncompSize, 22); // uncompressed size
    lfh.writeUInt16LE(nameBuf.length, 26);
    lfh.writeUInt16LE(0, 28);          // extra len

    lfhParts.push(lfh, nameBuf, data);
    const thisLfhOffset = offset;
    offset += lfh.length + nameBuf.length + data.length;

    // Central directory header (46 bytes)
    const cd = Buffer.alloc(46);
    cd.writeUInt32LE(SIG_CD, 0);
    cd.writeUInt16LE(entry.versionMadeBy ?? (3 << 8) | 20, 4); // OS=Unix(3), version=20
    cd.writeUInt16LE(20, 6);
    cd.writeUInt16LE(0, 8);
    cd.writeUInt16LE(0, 10);
    cd.writeUInt16LE(0, 12);
    cd.writeUInt16LE(0, 14);
    cd.writeUInt32LE(c, 16);
    cd.writeUInt32LE(compSize, 20);
    cd.writeUInt32LE(uncompSize, 24);
    cd.writeUInt16LE(nameBuf.length, 28);
    cd.writeUInt16LE(0, 30);
    cd.writeUInt16LE(0, 32); // comment len
    cd.writeUInt16LE(0, 34); // disk start
    cd.writeUInt16LE(0, 36); // internal attrs
    cd.writeUInt32LE((entry.externalAttr ?? 0) >>> 0, 38); // external attrs (unsigned)
    cd.writeUInt32LE(thisLfhOffset, 42);

    cdParts.push(cd, nameBuf);
  }

  const lfhSection = Buffer.concat(lfhParts);
  const cdSection = Buffer.concat(cdParts);
  const cdOffset = lfhSection.length;
  const cdSize = cdSection.length;

  const eocd = Buffer.alloc(22);
  eocd.writeUInt32LE(SIG_EOCD, 0);
  eocd.writeUInt16LE(0, 4);
  eocd.writeUInt16LE(0, 6);
  eocd.writeUInt16LE(entries.length, 8);
  eocd.writeUInt16LE(entries.length, 10);
  eocd.writeUInt32LE(cdSize, 12);
  eocd.writeUInt32LE(cdOffset, 16);
  eocd.writeUInt16LE(0, 20);

  return Buffer.concat([lfhSection, cdSection, eocd]);
}

/** Convenience: produce a unix mode in the upper 16 bits of externalAttr. */
export function unixModeAttr(mode) {
  return (mode & 0xFFFF) << 16;
}

export const MODE_SYMLINK = 0xA1FF; // S_IFLNK | rwxrwxrwx
