'use strict';

const fs = require('fs');
const path = require('path');
const logger = require('../../utils/logger');
const service = require('./billing-print.service');
const parser = require('./billing-print.parser');
const diagnostics = require('./billing-print.diagnostics');
const evidence = require('./billing-print.evidence');
const { getBillingPrintConfig } = require('./billing-print.config');
const ticketCapture = require('./billing-print.ticketCapture');

let watcher = null;
let interval = null;
let running = false;
let queued = false;
const state = {
  watcherRunning: false,
  status: 'disabled_by_config',
  lastPrnSeenAt: null,
  lastQrPrintedAt: null,
  lastError: null,
  startedAt: null,
  recentPrnDetected: [],
};
const recentHashes = new Map();
const captureBursts = new Map();
const MIN_FACTURABLE_BYTES = 9;
const DEFAULT_CAPTURE_DEBOUNCE_MS = 900;
const MIN_CAPTURE_DEBOUNCE_MS = 350;
const MAX_CAPTURE_DEBOUNCE_MS = 5000;
const MAX_SNAPSHOT_HISTORY = 10;

function emitEvent(level, event, payload = {}) {
  const fn = level === 'warn' ? logger.warn : logger.info;
  fn.call(logger, { event, ...payload });
  try { evidence.appendLog('ticket-capture.log', event, payload); } catch (_) {}
}

function pruneRecentHashes() {
  const cutoff = Date.now() - 10 * 60 * 1000;
  for (const [hash, at] of recentHashes.entries()) {
    if (at < cutoff) recentHashes.delete(hash);
  }
}

function snapshotDir(config) {
  const dir = path.join(config.watchFolder, 'snapshots');
  fs.mkdirSync(dir, { recursive: true });
  return dir;
}

function captureDebounceMs(config = {}) {
  if (config.captureDebounceMs !== undefined) {
    const requested = Number(config.captureDebounceMs) || DEFAULT_CAPTURE_DEBOUNCE_MS;
    if (config.allowShortCaptureDebounce === true) {
      return Math.max(25, Math.min(MAX_CAPTURE_DEBOUNCE_MS, requested));
    }
    return Math.max(MIN_CAPTURE_DEBOUNCE_MS, Math.min(MAX_CAPTURE_DEBOUNCE_MS, requested));
  }
  return Math.max(MIN_CAPTURE_DEBOUNCE_MS, Math.min(MAX_CAPTURE_DEBOUNCE_MS, DEFAULT_CAPTURE_DEBOUNCE_MS));
}

function capturePollMs(config = {}) {
  return Math.max(25, Math.min(250, Number(config.capturePollMs) || 100));
}

function captureStableWaitMs(config = {}) {
  return Math.max(100, Math.min(2000, Number(config.captureStableWaitMs || config.stableWaitMs) || 150));
}

function durationMs(start, end) {
  const left = Date.parse(start || '');
  const right = Date.parse(end || '');
  return Number.isFinite(left) && Number.isFinite(right) && right >= left ? right - left : null;
}

function happenedAtOrAfter(value, baseline) {
  const at = Date.parse(value || '');
  const since = Date.parse(baseline || '');
  return Number.isFinite(at) && Number.isFinite(since) && at >= since;
}

function rememberCaptureSnapshot(snapshot) {
  const current = Array.isArray(diagnostics.snapshot().lastCaptureSnapshots)
    ? diagnostics.snapshot().lastCaptureSnapshots
    : [];
  diagnostics.mark({
    lastCaptureSnapshots: [
      {
        file: snapshot.file,
        snapshot: path.basename(snapshot.path),
        size: snapshot.size,
        facturable: snapshot.facturable === true,
        capturedAt: snapshot.capturedAt,
      },
      ...current,
    ].slice(0, MAX_SNAPSHOT_HISTORY),
  });
}

function incrementIgnoredSmallJobs() {
  const current = Number(diagnostics.snapshot().ignoredSmallJobsCount) || 0;
  diagnostics.mark({ ignoredSmallJobsCount: current + 1 });
}

function parseEscPosControlOnly(buffer) {
  let i = 0;
  while (i < buffer.length) {
    const byte = buffer[i];
    if (byte === 0x00 || byte === 0x09 || byte === 0x0a || byte === 0x0b || byte === 0x0c || byte === 0x0d || byte === 0x20) {
      i += 1;
      continue;
    }
    if (byte === 0x1b) {
      const cmd = buffer[i + 1];
      if (cmd === 0x40 || cmd === 0x69 || cmd === 0x6d) {
        i += 2;
        continue;
      }
      if ((cmd === 0x64 || cmd === 0x4a) && i + 2 < buffer.length) {
        i += 3;
        continue;
      }
      return false;
    }
    if (byte === 0x1d && buffer[i + 1] === 0x56) {
      i += i + 2 < buffer.length ? 3 : 2;
      continue;
    }
    return false;
  }
  return buffer.length > 0;
}

function isCutOnlyBuffer(buffer) {
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) return false;
  if (parseEscPosControlOnly(buffer)) return true;
  const printable = buffer
    .toString('latin1')
    .replace(/[\x00-\x1f\x7f]/g, '')
    .trim();
  return printable.length === 0;
}

function isFacturableSnapshot(buffer) {
  try {
    return parser.parseTicket(buffer.toString('latin1')).isNotaConsumo === true;
  } catch (_) {
    return false;
  }
}

async function copyCandidate(filePath, config, seenHashes = new Set()) {
  try {
    if (!fs.existsSync(filePath)) return null;
    const stat = fs.statSync(filePath);
    if (!stat.isFile()) return null;
    if (stat.size === 0) {
      emitEvent('warn', 'CAPTURE_FILE_EMPTY', { file: path.basename(filePath), path: filePath });
      return null;
    }
    const buffer = fs.readFileSync(filePath);
    const hash = require('crypto').createHash('sha256').update(buffer).digest('hex');
    if (seenHashes.has(hash)) return null;
    seenHashes.add(hash);
    if (isCutOnlyBuffer(buffer)) {
      incrementIgnoredSmallJobs();
      emitEvent('info', 'CAPTURE_CUT_ONLY_JOB_IGNORED', { file: path.basename(filePath), path: filePath, bytes: buffer.length });
      return null;
    }
    if (buffer.length <= 8) {
      incrementIgnoredSmallJobs();
      emitEvent('info', 'CAPTURE_SMALL_JOB_IGNORED', { file: path.basename(filePath), path: filePath, bytes: buffer.length });
      return null;
    }
    pruneRecentHashes();
    if (recentHashes.has(hash)) {
      emitEvent('info', 'CAPTURE_FILE_DUPLICATE_SKIPPED', { file: path.basename(filePath), path: filePath });
      return null;
    }
    const targetDir = snapshotDir(config);
    const target = path.join(targetDir, `${Date.now()}-${path.basename(filePath)}`);
    fs.writeFileSync(target, buffer);
    const snapshot = {
      path: target,
      hash,
      size: buffer.length,
      file: path.basename(filePath),
      capturedAt: new Date().toISOString(),
      facturable: buffer.length >= MIN_FACTURABLE_BYTES && isFacturableSnapshot(buffer),
    };
    rememberCaptureSnapshot(snapshot);
    emitEvent('info', 'CAPTURE_SNAPSHOT_TAKEN', {
      file: snapshot.file,
      snapshot: path.basename(snapshot.path),
      bytes: snapshot.size,
      facturable: snapshot.facturable,
    });
    return snapshot;
  } catch (err) {
    logger.warn({ event: 'billing_print_snapshot_failed', file: path.basename(filePath), error: err.message });
    return null;
  }
}

async function processSnapshot(snapshot, config, pipelineTiming = {}) {
  if (!snapshot) return null;
  const snapshotPath = typeof snapshot === 'string' ? snapshot : snapshot.path;
  const dir = path.dirname(snapshotPath);
  try {
    const result = await service.processPrnFile(snapshotPath, {
      ...config,
      watchFolder: dir,
      cleanupPrn: false,
      snapshotAlreadyStable: true,
      captureSource: snapshot.captureSource || config.captureSource,
      pipelineTiming: {
        captureCreatedAt: snapshot.captureCreatedAt || pipelineTiming.captureCreatedAt || null,
        snapshotSelectedAt: snapshot.snapshotSelectedAt || pipelineTiming.snapshotSelectedAt || null,
      },
    });
    if (result?.ok) {
      if (result.qrGenerated) state.lastQrPrintedAt = new Date().toISOString();
      state.lastError = null;
    }
    return result;
  } finally {
    try { fs.unlinkSync(snapshotPath); } catch (_) {}
  }
}

function resolveCaptureSource(filePath, config = {}) {
  const name = path.basename(filePath || '');
  if (/^arkhon-capture-test-/i.test(name) || /^arkhon-test-ticket-/i.test(name)) return 'test_print';
  const base = String(config.basePrnName || 'Trabajo de FoxPro definido por el usuario.prn').trim().toLowerCase();
  const lower = name.toLowerCase();
  const ext = path.extname(base);
  const stem = ext ? base.slice(0, -ext.length) : base;
  const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const escapedExt = ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  if (lower === base || (ext && new RegExp(`^${escapedStem} \\(\\d+\\)${escapedExt}$`).test(lower))) return 'softrestaurant_real';
  return lower.endsWith('.prn') ? 'manual_prn' : 'unknown';
}

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function selectBestSnapshot(snapshots) {
  const candidates = snapshots.filter(Boolean);
  const facturable = candidates.filter((snapshot) => snapshot.facturable);
  const pool = facturable.length ? facturable : candidates;
  return pool
    .slice()
    .sort((a, b) => (b.size - a.size) || (Date.parse(a.capturedAt) - Date.parse(b.capturedAt)))[0] || null;
}

async function captureLargestCandidate(filePath, config, pipelineTiming = {}) {
  const snapshots = [];
  const seenHashes = new Set();
  const started = Date.now();
  const deadline = started + captureDebounceMs(config);
  const stableMs = captureStableWaitMs(config);
  let bestSignature = null;
  let bestStableSince = null;
  while (Date.now() <= deadline) {
    const snapshot = await copyCandidate(filePath, config, seenHashes);
    if (snapshot) snapshots.push(snapshot);
    const currentBest = selectBestSnapshot(snapshots);
    if (currentBest?.facturable === true) {
      const signature = `${currentBest.hash}:${currentBest.size}`;
      if (signature !== bestSignature) {
        bestSignature = signature;
        bestStableSince = Date.now();
      } else if (bestStableSince && Date.now() - bestStableSince >= stableMs) {
        break;
      }
    }
    await wait(capturePollMs(config));
  }
  const selected = selectBestSnapshot(snapshots);
  if (selected) {
    selected.captureCreatedAt = pipelineTiming.captureCreatedAt || selected.capturedAt || new Date(started).toISOString();
    selected.snapshotSelectedAt = new Date().toISOString();
  }
  for (const snapshot of snapshots) {
    if (snapshot.path !== selected?.path) {
      try { fs.unlinkSync(snapshot.path); } catch (_) {}
    }
  }
  return selected;
}

function recordPrnDetected(filePath, event = 'change') {
  const previous = state.recentPrnDetected.find((item) => item.path === filePath);
  const detected = { name: path.basename(filePath), path: filePath, event, detectedAt: new Date().toISOString() };
  state.lastPrnSeenAt = detected.detectedAt;
  state.recentPrnDetected = [detected, ...state.recentPrnDetected.filter((item) => item.path !== filePath)].slice(0, 10);
  diagnostics.mark({ captureCreatedAt: detected.detectedAt });
  if (previous && Date.now() - Date.parse(previous.detectedAt) < 1000) return detected;
  logger.info({ event: 'CAPTURE_FILE_CREATED', file: detected.name, path: filePath, watchEvent: event });
  try { evidence.appendLog('ticket-capture.log', 'CAPTURE_FILE_CREATED', { file: detected.name, path: filePath, watchEvent: event }); } catch (_) {}
  return detected;
}

function removeSourceIfUnchanged(filePath, capturedHash, config) {
  try {
    if (!fs.existsSync(filePath)) return true;
    const resolvedFolder = path.resolve(config.watchFolder);
    const resolvedFile = path.resolve(filePath);
    if (!resolvedFile.startsWith(`${resolvedFolder}${path.sep}`) || path.extname(resolvedFile).toLowerCase() !== '.prn') return false;
    const currentHash = require('crypto').createHash('sha256').update(fs.readFileSync(resolvedFile)).digest('hex');
    if (currentHash !== capturedHash) return false;
    fs.unlinkSync(resolvedFile);
    return true;
  } catch (error) {
    logger.warn({ event: 'billing_print_source_cleanup_failed', file: path.basename(filePath), error: error.message });
    return false;
  }
}

function removeCutOnlySource(filePath, config) {
  try {
    if (!fs.existsSync(filePath)) return false;
    const resolvedFolder = path.resolve(config.watchFolder);
    const resolvedFile = path.resolve(filePath);
    if (!resolvedFile.startsWith(`${resolvedFolder}${path.sep}`) || path.extname(resolvedFile).toLowerCase() !== '.prn') return false;
    const buffer = fs.readFileSync(resolvedFile);
    if (!isCutOnlyBuffer(buffer)) return false;
    fs.unlinkSync(resolvedFile);
    return true;
  } catch (error) {
    logger.warn({ event: 'billing_print_cut_only_cleanup_failed', file: path.basename(filePath), error: error.message });
    return false;
  }
}

function queueCaptureBurst(filePath, config) {
  if (captureBursts.has(filePath)) return captureBursts.get(filePath);
  const recent = state.recentPrnDetected.find((item) => item.path === filePath);
  const captureCreatedAt = recent?.detectedAt || new Date().toISOString();
  diagnostics.mark({ captureCreatedAt });
  const task = (async () => {
    try {
      const captured = await captureLargestCandidate(filePath, config, { captureCreatedAt });
      if (captured?.path) {
        const captureToStableMs = durationMs(captureCreatedAt, captured.snapshotSelectedAt);
        emitEvent('info', 'CAPTURE_FILE_STABLE', { file: path.basename(filePath), snapshot: path.basename(captured.path), bytes: captured.size, captureToStableMs });
        emitEvent('info', 'CAPTURE_SNAPSHOT_SELECTED', {
          file: path.basename(filePath),
          snapshot: path.basename(captured.path),
          bytes: captured.size,
          facturable: captured.facturable,
          snapshotSelectedAt: captured.snapshotSelectedAt,
          captureToStableMs,
        });
        diagnostics.mark({
          snapshotSelectedAt: captured.snapshotSelectedAt,
          captureToStableMs,
          selectedSnapshotSize: captured.size,
        });
        try {
          if (fs.existsSync(filePath)) {
            const currentSize = fs.statSync(filePath).size;
            if (currentSize > 0 && currentSize < captured.size) {
              diagnostics.mark({ overwriteProtected: true });
              emitEvent('warn', 'CAPTURE_OVERWRITE_PROTECTED', {
                file: path.basename(filePath),
                selectedSnapshot: path.basename(captured.path),
                selectedBytes: captured.size,
                currentBytes: currentSize,
              });
            }
          }
        } catch (_) {}
        const at = new Date().toISOString();
        const captureSource = resolveCaptureSource(filePath, config);
        captured.captureSource = captureSource;
        ticketCapture.markTicketCaptured(at, { source: captureSource });
        diagnostics.mark({ lastTicketCapturedAt: at, lastCaptureSource: captureSource });
        // La prueba manual debe recorrer el mismo pipeline que un ticket real,
        // incluido token Cloud y QR; de otro modo daba un falso negativo.
        const result = await processSnapshot(captured, config, {
          captureCreatedAt,
          snapshotSelectedAt: captured.snapshotSelectedAt,
        });
        recentHashes.set(captured.hash, Date.now());
        removeSourceIfUnchanged(filePath, captured.hash, config);
        return result;
      }
      return { ok: false, processed: false, reason: 'CAPTURE_FILE_EMPTY', cutOnlyRemoved: removeCutOnlySource(filePath, config) };
    } catch (err) {
      state.lastError = err.message;
      emitEvent('warn', 'WATCHER_ERROR', { file: path.basename(filePath), error: err.message });
    } finally {
      captureBursts.delete(filePath);
    }
  })();
  captureBursts.set(filePath, task);
  return task;
}

async function drain(config) {
  if (running) {
    queued = true;
    return;
  }
  running = true;
  try {
    for (let attempt = 0; attempt < (config.maxPrnFiles || 100); attempt += 1) {
      queued = false;
      const job = await service.elegirJobNota(config);
      if (!job) break;
      recordPrnDetected(job, 'drain');
      const result = await queueCaptureBurst(job, config);
      if (result?.ok && result.qrGenerated) state.lastQrPrintedAt = new Date().toISOString();
      if (result?.ok) state.lastError = null;
      if (!result) break;
    }
  } catch (err) {
    state.lastError = err.message;
    logger.warn({ event: 'billing_print_drain_failed', error: err.message });
  } finally {
    running = false;
  }
}

function startBillingPrintWatcher(runtimeContext = null) {
  const config = getBillingPrintConfig();
  diagnostics.mark({
    serviceRuntimeHasBillingPrint: true,
    processPid: process.pid,
    lastRuntimeContext: runtimeContext || diagnostics.snapshot().lastRuntimeContext || null,
  });
  if (!config.enabled || config.autoStart === false) {
    state.watcherRunning = false;
    state.status = 'disabled_by_config';
    state.lastError = !config.enabled ? 'billing-print disabled by config' : 'billing-print autoStart disabled';
    emitEvent('warn', 'WATCHER_DISABLED', { reason: state.lastError, pid: process.pid, enabled: config.enabled, autoStart: config.autoStart });
    return { enabled: false, started: false, status: state.status };
  }
  if (!config.watchFolder || !fs.existsSync(config.watchFolder)) {
    state.watcherRunning = false;
    state.status = 'pending_config';
    state.lastError = 'watchFolder missing';
    logger.warn({ event: 'billing_print_not_started', reason: 'watchFolder missing' });
    emitEvent('warn', 'WATCHER_DISABLED', { reason: 'watchFolder_missing', pid: process.pid, watchFolder: config.watchFolder || null });
    return { enabled: true, started: false, status: state.status, reason: 'watchFolder_missing' };
  }
  if (!config.outputPrinterName && !config.printerName) {
    state.watcherRunning = false;
    state.status = 'pending_config';
    state.lastError = 'outputPrinterName missing';
    logger.warn({ event: 'billing_print_not_started', reason: 'outputPrinterName missing' });
    emitEvent('warn', 'WATCHER_DISABLED', { reason: 'outputPrinterName_missing', pid: process.pid, watchFolder: config.watchFolder });
    return { enabled: true, started: false, status: state.status, reason: 'outputPrinterName_missing' };
  }
  if (String(config.outputPrinterName || config.printerName).trim().toLowerCase() === String(config.capturePrinterName || 'ARKHON Ticket Capture').trim().toLowerCase()) {
    state.watcherRunning = false;
    state.status = 'pending_config';
    state.lastError = 'outputPrinterName no puede ser ARKHON Ticket Capture';
    logger.warn({ event: 'billing_print_not_started', reason: 'outputPrinterName_is_capture_printer' });
    emitEvent('warn', 'WATCHER_DISABLED', { reason: 'outputPrinterName_is_capture_printer', pid: process.pid, outputPrinter: config.outputPrinterName || config.printerName });
    return { enabled: true, started: false, status: state.status, reason: 'outputPrinterName_is_capture_printer' };
  }

  stopBillingPrintWatcher();
  watcher = fs.watch(config.watchFolder, { persistent: false }, (eventType, filename) => {
    if (!filename || path.extname(String(filename)).toLowerCase() !== '.prn') return;
    const fullPath = path.join(config.watchFolder, String(filename));
    recordPrnDetected(fullPath, eventType);
    queueCaptureBurst(fullPath, config);
  });
  interval = setInterval(() => drain(config), 5000);
  interval.unref?.();
  drain(config);
  state.watcherRunning = true;
  state.status = 'active';
  state.startedAt = new Date().toISOString();
  state.lastError = null;
  emitEvent('info', 'WATCHER_RUNNING', {
    folder: config.watchFolder,
    outputPrinter: config.outputPrinterName || config.printerName,
    pid: process.pid,
    licenseStatus: runtimeContext?.license?.status || null,
    qrAllowed: runtimeContext?.decisions?.qrAllowed === true,
    qrBlockedCause: runtimeContext?.decisions?.qrBlockedCause || null,
  });
  return { enabled: true, started: true, status: state.status };
}

async function createAndProcessCaptureTestPrn(config = getBillingPrintConfig()) {
  if (!state.watcherRunning || !watcher) {
    return { ok: false, processed: false, reason: 'watcher_not_running' };
  }
  const fileName = `arkhon-capture-test-${Date.now()}.prn`;
  const testFile = path.join(config.watchFolder, fileName);
  const fixture = [
    '***SOFT RESTAURANT TEST***',
    'NOTA DE CONSUMO',
    `FOLIO: TEST-${Date.now()}`,
    'CODIGO DE FACTURACION: TESTCAPTURE01',
    'TOTAL: $1.00',
    'PRUEBA MANUAL ARKHON TICKET CAPTURE',
    '',
  ].join('\r\n');
  fs.writeFileSync(testFile, Buffer.from(fixture, 'latin1'), { flag: 'wx' });
  recordPrnDetected(testFile, 'test_print');
  const result = await queueCaptureBurst(testFile, config);
  return { ...(result || { ok: false, reason: 'capture_test_not_processed' }), processed: result?.ok === true, testFile };
}

async function startSoftRestaurantRealTest(input = {}, config = getBillingPrintConfig()) {
  const timeoutMs = Math.max(1000, Math.min(120000, Number(input.timeoutMs) || 120000));
  const startedAt = new Date().toISOString();
  await service.borrarTodosLosPrn(config.watchFolder, config);
  const deadline = Date.now() + timeoutMs;
  let lastResult = null;
  let detected = false;
  while (Date.now() < deadline) {
    const job = await service.elegirJobNota(config);
    if (job) {
      detected = true;
      recordPrnDetected(job, 'softrestaurant_real_test');
      lastResult = await queueCaptureBurst(job, config);
      if (lastResult?.ok === true) break;
    }
    await wait(Math.min(500, Math.max(50, Number(input.pollMs) || 250)));
  }
  const prnDetected = detected || Boolean(lastResult || state.recentPrnDetected.find((item) => Date.parse(item.detectedAt) >= Date.parse(startedAt)));
  const printDiagnostics = diagnostics.snapshot();
  const watcherPrintedRealTicket = printDiagnostics.lastCaptureSource === 'softrestaurant_real'
    && happenedAtOrAfter(printDiagnostics.lastTicketCapturedAt, startedAt)
    && happenedAtOrAfter(printDiagnostics.lastRawPrintSuccessAt, startedAt);
  const watcherPrintedQr = watcherPrintedRealTicket
    && printDiagnostics.lastQrDecision?.allowed === true
    && happenedAtOrAfter(printDiagnostics.lastQrPrintedAt, startedAt);
  if (!prnDetected) {
    return {
      ok: true,
      softRestaurantPrintingToCapture: false,
      state: 'softrestaurant_misconfigured',
      prnDetected: false,
      parsed: false,
      tokenCreated: false,
      qrPrinted: false,
      printedToOutput: false,
      recommendedAction: 'Configure Cuentas (remisiones) y Notas de consumo con ARKHON Ticket Capture.',
    };
  }
  return {
    ok: lastResult?.ok === true || watcherPrintedRealTicket,
    softRestaurantPrintingToCapture: true,
    prnDetected: true,
    parsed: lastResult?.ok === true || watcherPrintedRealTicket,
    tokenCreated: lastResult?.qrGenerated === true || watcherPrintedQr,
    qrPrinted: lastResult?.qrGenerated === true || watcherPrintedQr,
    printedToOutput: (lastResult?.ok === true && lastResult?.processed !== false) || watcherPrintedRealTicket,
    processing: lastResult,
    recommendedAction: null,
  };
}

function stopBillingPrintWatcher() {
  if (watcher) {
    try { watcher.close(); } catch (_) {}
    watcher = null;
  }
  if (interval) {
    clearInterval(interval);
    interval = null;
  }
  state.watcherRunning = false;
  if (state.status === 'active') state.status = 'stopped';
  return { stopped: true, status: state.status };
}

function getBillingPrintWatcherState() {
  return { ...state, watcherRunning: Boolean(watcher && interval && state.watcherRunning) };
}

module.exports = {
  startBillingPrintWatcher,
  stopBillingPrintWatcher,
  getBillingPrintWatcherState,
  createAndProcessCaptureTestPrn,
  startSoftRestaurantRealTest,
  _test: {
    captureLargestCandidate,
    copyCandidate,
    isCutOnlyBuffer,
    selectBestSnapshot,
  },
};
