#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Coverage Gate for Quality Gates
 *
 * WU-1433: Adds coverage checking to gates with configurable mode (warn/block).
 * Enforces ≥90% coverage on hex core files (application layer).
 *
 * Mode flag allows gradual rollout:
 * - warn: Log failures but don't block (default)
 * - block: Fail the gate if thresholds not met
 *
 * @see {@link packages/@lumenflow/cli/src/gates.ts} - Integration point
 * @see {@link vitest.config.ts} - Coverage thresholds
 */

/* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
import { readFileSync, existsSync } from 'node:fs';
import { EMOJI, STRING_LITERALS } from '../constants/wu-ui-constants.js';
import { DEFAULT_MIN_COVERAGE } from '../constants/gate-constants.js';

/**
 * Coverage gate modes
 * @constant
 */
export const COVERAGE_GATE_MODES = Object.freeze({
  /** Log warnings but don't fail the gate */
  WARN: 'warn',
  /** Fail the gate if thresholds not met */
  BLOCK: 'block',
});

/**
 * Glob patterns for hex core files that require ≥90% coverage.
 * These are the critical application layer files.
 *
 * WU-1068: Changed from @exampleapp to @lumenflow for framework reusability.
 * Project-specific patterns should be configured in workspace.yaml.
 *
 * @constant {string[]}
 */
export const HEX_CORE_PATTERNS = Object.freeze([
  'packages/@lumenflow/core/',
  'packages/@lumenflow/cli/',
]);

/**
 * Default coverage threshold for hex core files (percentage)
 * WU-1262: Kept for backwards compatibility.
 * WU-2044: Now delegates to DEFAULT_MIN_COVERAGE from gate-constants.ts (DRY).
 * The actual threshold is determined by resolveCoverageConfig() based on methodology.
 * @constant {number}
 */
export const COVERAGE_THRESHOLD = DEFAULT_MIN_COVERAGE;

/**
 * Default path to coverage summary JSON
 * @constant {string}
 */
export const DEFAULT_COVERAGE_PATH = 'coverage/coverage-summary.json';

/**
 * Check if a file path is in the hex core layer.
 *
 * WU-2448: Coverage reporters may emit absolute paths (e.g., <repo-root>/packages/...)
 * or file:// URLs; use substring matching so hex-core checks still apply.
 *
 * @param {string|null|undefined} filePath - File path to check
 * @returns {boolean} True if file is in hex core layer
 */
export function isHexCoreFile(filePath: UnsafeAny) {
  if (!filePath || typeof filePath !== 'string') {
    return false;
  }

  // Normalize backslashes to forward slashes for cross-platform compatibility
  const normalizedPath = filePath.replace(/\\/g, '/');
  return HEX_CORE_PATTERNS.some((pattern) => normalizedPath.includes(pattern));
}

/**
 * Parse coverage JSON file.
 *
 * @param {string} coveragePath - Path to coverage-summary.json
 * @returns {object|null} Parsed coverage data or null if invalid
 */
export function parseCoverageJson(coveragePath: UnsafeAny) {
  if (!existsSync(coveragePath)) {
    return null;
  }

  try {
    const content = readFileSync(coveragePath, { encoding: 'utf-8' });
    const data = JSON.parse(content);

    // Transform to consistent format
    const files: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      if (key === 'total') continue;
      files[key] = value;
    }

    return {
      total: data.total,
      files,
    };
  } catch {
    return null;
  }
}

/**
 * Check if coverage meets thresholds for hex core files.
 *
 * @param {object|null} coverageData - Parsed coverage data
 * @param {number} [threshold] - Coverage threshold to use (defaults to COVERAGE_THRESHOLD)
 * @returns {{ pass: boolean, failures: Array<{ file: string, actual: number, threshold: number, metric: string }> }}
 */
export function checkCoverageThresholds(coverageData: UnsafeAny, threshold?: number) {
  if (!coverageData || !coverageData.files) {
    return { pass: true, failures: [] };
  }

  // WU-1262: Use provided threshold or fall back to constant
  const effectiveThreshold = threshold ?? COVERAGE_THRESHOLD;
  const failures = [];

  for (const [file, metricsValue] of Object.entries(coverageData.files)) {
    if (!isHexCoreFile(file)) {
      continue;
    }

    // Check lines coverage (primary metric)
    const metrics = metricsValue as { lines?: { pct: number } };
    const linesCoverage = metrics.lines?.pct ?? 0;
    if (linesCoverage < effectiveThreshold) {
      failures.push({
        file,
        actual: linesCoverage,
        threshold: effectiveThreshold,
        metric: 'lines',
      });
    }
  }

  return {
    pass: failures.length === 0,
    failures,
  };
}

/**
 * Format coverage data for display.
 *
 * @param {object|null} coverageData - Parsed coverage data
 * @returns {string} Formatted output string
 */
export function formatCoverageDelta(coverageData: UnsafeAny) {
  if (!coverageData) {
    return '';
  }

  const lines = [];
  const totalPct = coverageData.total?.lines?.pct ?? 0;

  lines.push(
    `${STRING_LITERALS.NEWLINE}Coverage Summary: ${totalPct.toFixed(1)}% lines${STRING_LITERALS.NEWLINE}`,
  );

  // Show hex core files
  const hexCoreFiles = Object.entries(coverageData.files || {}).filter(([file]) =>
    isHexCoreFile(file),
  );

  if (hexCoreFiles.length > 0) {
    lines.push('Hex Core Files:');
    for (const [file, metricsValue] of hexCoreFiles) {
      const metrics = metricsValue as { lines?: { pct: number } };
      const pct = metrics.lines?.pct ?? 0;
      const status = pct >= COVERAGE_THRESHOLD ? EMOJI.SUCCESS : EMOJI.FAILURE;
      const shortFile = file.replace('packages/@lumenflow/', '');
      lines.push(`  ${status} ${shortFile}: ${pct.toFixed(1)}%`);
    }
  }

  return lines.join(STRING_LITERALS.NEWLINE);
}

/**
 * Logger interface for coverage gate output
 */
interface CoverageGateLogger {
  log: (...args: unknown[]) => void;
}

/**
 * Options for running coverage gate
 */
export interface CoverageGateOptions {
  /** Gate mode ('warn' or 'block') */
  mode?: string;
  /** Path to coverage JSON */
  coveragePath?: string;
  /** Logger for output */
  logger?: CoverageGateLogger;
  /**
   * WU-1262: Coverage threshold override (0-100).
   * When provided, overrides the default COVERAGE_THRESHOLD constant.
   * This is typically populated from resolveCoverageConfig().
   */
  threshold?: number;
}

/**
 * Run coverage gate.
 *
 * @param {CoverageGateOptions} options - Gate options
 * @returns {Promise<{ ok: boolean, mode: string, duration: number, message: string }>}
 */
export async function runCoverageGate(options: CoverageGateOptions = {}) {
  const start = Date.now();
  const mode = options.mode || COVERAGE_GATE_MODES.WARN;
  const coveragePath = options.coveragePath || DEFAULT_COVERAGE_PATH;
  const logger =
    options.logger && typeof options.logger.log === 'function' ? options.logger : console;
  // WU-1262: Use provided threshold or default constant
  const threshold = options.threshold ?? COVERAGE_THRESHOLD;

  // Parse coverage data
  const coverageData = parseCoverageJson(coveragePath);

  if (!coverageData) {
    const duration = Date.now() - start;
    logger.log(`\n${EMOJI.WARNING} Coverage gate: No coverage data found at ${coveragePath}`);
    logger.log('  Run tests with coverage first: pnpm test:coverage\n');
    return { ok: true, mode, duration, message: 'No coverage data' };
  }

  // Check thresholds (WU-1262: pass resolved threshold)
  const { pass, failures } = checkCoverageThresholds(coverageData, threshold);

  // Format and display
  const output = formatCoverageDelta(coverageData);
  logger.log(output);

  const duration = Date.now() - start;

  if (!pass) {
    logger.log(`\n${EMOJI.FAILURE} Coverage below ${threshold}% for hex core files:`);
    for (const failure of failures) {
      const shortFile = failure.file.replace('packages/@lumenflow/', '');
      logger.log(
        `  - ${shortFile}: ${failure.actual.toFixed(1)}% (requires ${failure.threshold}%)`,
      );
    }

    if (mode === COVERAGE_GATE_MODES.BLOCK) {
      logger.log(`\n${EMOJI.FAILURE} Coverage gate FAILED (mode: block)\n`);
      return { ok: false, mode, duration, message: 'Coverage threshold not met' };
    } else {
      logger.log(`\n${EMOJI.WARNING} Coverage gate WARNING (mode: warn)\n`);
      logger.log('  Note: This will become blocking in future. Fix coverage now.\n');
      return { ok: true, mode, duration, message: 'Coverage warning' };
    }
  }

  logger.log(`\n${EMOJI.SUCCESS} Coverage gate passed\n`);
  return { ok: true, mode, duration, message: 'Coverage OK' };
}
