// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Gates Plan Resolvers
 *
 * WU-1647: Extracted pure plan-resolution functions that decide
 * what mode (full/incremental/skip) each gate should use.
 *
 * These functions are side-effect-free and only depend on their inputs.
 *
 * @module gates-plan-resolvers
 */
import { existsSync } from 'node:fs';
import path from 'node:path';
import { isLintableFile } from '@lumenflow/core/incremental-lint';
import { getBasename, normalizePath, extractPackagesFromCodePaths } from './gates-utils.js';
// ── Config file detection ──────────────────────────────────────────────
const PRETTIER_CONFIG_FILES = new Set([
    '.prettierrc',
    '.prettierrc.json',
    '.prettierrc.yaml',
    '.prettierrc.yml',
    '.prettierrc.js',
    '.prettierrc.cjs',
    '.prettierrc.ts',
    'prettier.config.js',
    'prettier.config.cjs',
    'prettier.config.ts',
    'prettier.config.mjs',
    '.prettierignore',
]);
const WORKSPACE_ARTIFACT_ROOT_PREFIXES = ['packages/', 'apps/', 'tools/'];
const DIST_ARTIFACT_ROOT_SUFFIX = '/dist';
const DIST_ARTIFACT_PATH_SEGMENT = '/dist/';
// WU-1356: Extended to support multiple build tools and test runners
const TEST_CONFIG_BASENAMES = new Set([
    'turbo.json', // Turborepo
    'nx.json', // Nx
    'lerna.json', // Lerna
    'pnpm-lock.yaml',
    'package-lock.json',
    'yarn.lock',
    'bun.lockb',
    'package.json',
]);
// WU-1356: Extended to support vitest, jest, and mocha config patterns
const TEST_CONFIG_PATTERNS = [
    /^vitest\.config\.(ts|mts|js|mjs|cjs)$/i,
    /^jest\.config\.(ts|js|mjs|cjs|json)$/i,
    /^\.mocharc\.(js|json|yaml|yml)$/i,
    // eslint-disable-next-line security/detect-unsafe-regex -- static tsconfig pattern; no backtracking risk
    /^tsconfig(\..+)?\.json$/i,
];
export function isPrettierConfigFile(filePath) {
    if (!filePath)
        return false;
    const basename = getBasename(filePath);
    return PRETTIER_CONFIG_FILES.has(basename);
}
function trimTrailingPathSeparators(filePath) {
    let trimmed = filePath;
    while (trimmed.endsWith('/')) {
        trimmed = trimmed.slice(0, -1);
    }
    return trimmed;
}
export function isWorkspaceDistArtifactRoot(filePath) {
    if (!filePath) {
        return false;
    }
    const normalizedPath = trimTrailingPathSeparators(normalizePath(filePath));
    if (!normalizedPath) {
        return false;
    }
    const hasWorkspacePrefix = WORKSPACE_ARTIFACT_ROOT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix));
    const isDistArtifactPath = normalizedPath.endsWith(DIST_ARTIFACT_ROOT_SUFFIX) ||
        normalizedPath.includes(DIST_ARTIFACT_PATH_SEGMENT);
    return hasWorkspacePrefix && isDistArtifactPath;
}
export function filterFormatCheckChangedFiles(changedFiles) {
    return changedFiles.filter((filePath) => !isWorkspaceDistArtifactRoot(filePath));
}
function filterExistingFormatCheckFiles(changedFiles, cwd) {
    return changedFiles.filter((filePath) => {
        const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
        return existsSync(resolvedPath);
    });
}
export function isTestConfigFile(filePath) {
    if (!filePath)
        return false;
    const basename = getBasename(filePath);
    if (TEST_CONFIG_BASENAMES.has(basename)) {
        return true;
    }
    return TEST_CONFIG_PATTERNS.some((pattern) => pattern.test(basename));
}
// ── Plan resolvers ─────────────────────────────────────────────────────
/* eslint-disable sonarjs/no-duplicate-string -- Pre-existing: format check reasons are intentionally distinct string literals */
export function resolveFormatCheckPlan({ changedFiles, fileListError = false, cwd, }) {
    const filteredChangedFiles = filterFormatCheckChangedFiles(changedFiles);
    if (fileListError) {
        return { mode: 'full', files: [], reason: 'file-list-error' };
    }
    if (filteredChangedFiles.some(isPrettierConfigFile)) {
        return { mode: 'full', files: [], reason: 'prettier-config' };
    }
    const existingChangedFiles = cwd
        ? filterExistingFormatCheckFiles(filteredChangedFiles, cwd)
        : filteredChangedFiles;
    if (existingChangedFiles.length === 0) {
        return { mode: 'skip', files: [] };
    }
    return { mode: 'incremental', files: existingChangedFiles };
}
export function resolveLintPlan({ isMainBranch, changedFiles, }) {
    if (isMainBranch) {
        return { mode: 'full', files: [] };
    }
    const lintTargets = changedFiles.filter((filePath) => {
        const normalized = normalizePath(filePath);
        return isLintableFile(normalized);
    });
    if (lintTargets.length === 0) {
        return { mode: 'skip', files: [] };
    }
    return { mode: 'incremental', files: lintTargets };
}
/* eslint-enable sonarjs/no-duplicate-string */
export function resolveTestPlan({ isMainBranch, hasUntrackedCode, hasConfigChange, fileListError, }) {
    if (fileListError) {
        return { mode: 'full', reason: 'file-list-error' };
    }
    if (hasUntrackedCode) {
        return { mode: 'full', reason: 'untracked-code' };
    }
    if (hasConfigChange) {
        return { mode: 'full', reason: 'test-config' };
    }
    if (isMainBranch) {
        return { mode: 'full' };
    }
    return { mode: 'incremental' };
}
/**
 * WU-1299: Resolve test plan for docs-only mode
 */
export function resolveDocsOnlyTestPlan({ codePaths, cwd, }) {
    const packages = extractPackagesFromCodePaths(codePaths, { cwd });
    if (packages.length === 0) {
        return {
            mode: 'skip',
            packages: [],
            reason: 'no-code-packages',
        };
    }
    return {
        mode: 'filtered',
        packages,
    };
}
/**
 * WU-1299: Format message for docs-only test skipping/filtering
 */
export function formatDocsOnlySkipMessage(plan) {
    if (plan.mode === 'skip') {
        return '\uD83D\uDCDD docs-only mode: skipping all tests (no code packages in code_paths)';
    }
    const packageList = plan.packages.join(', ');
    return `\uD83D\uDCDD docs-only mode: running tests only for packages in code_paths: ${packageList}`;
}
/**
 * Resolve spec-linter execution strategy.
 * If current WU is known, run scoped validation only.
 * If unknown, fall back to global validation.
 */
export function resolveSpecLinterPlan(wuId) {
    if (wuId) {
        return {
            scopedWuId: wuId,
            runGlobal: false,
        };
    }
    return {
        scopedWuId: null,
        runGlobal: true,
    };
}
//# sourceMappingURL=gates-plan-resolvers.js.map