// Test/source pairing adapter (Epic #177, Issue #180). Deterministic, pure-JS mapper that,
// given a workspace-relative path or a bare symbol name, locates the paired test (or paired
// source) file using fixed name conventions. Output normalized to EvidenceAtom via the shared
// buildAtom helper. Stays within ADR-0019 rule 3b: imports only @oscharko-dev/keiko-contracts,
// sibling workspace modules, and Node stdlib (node:crypto).
import { createHash } from "node:crypto";
import { resolveWithinWorkspace } from "./paths.js";
import { assertContainedRealPath } from "./realpath.js";
import { buildAtom, gatherCandidates } from "./repoSearchScan.js";
// Canonical fingerprint shared by every structural adapter: SHA-256({kind,text}) → 16 hex chars.
function queryFingerprint(query) {
    const canonical = JSON.stringify({ kind: query.kind, text: query.text });
    return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
}
const PATH_EXT_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
const TEST_MARKER_RE = /\.(?:test|spec)\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
function looksLikePath(text) {
    return PATH_EXT_RE.test(text);
}
function extractExtension(path) {
    const match = PATH_EXT_RE.exec(path);
    if (match === null) {
        return undefined;
    }
    return { stem: path.slice(0, match.index), ext: match[0] };
}
function basenameOf(stem) {
    const slash = stem.lastIndexOf("/");
    if (slash === -1) {
        return { dir: "", base: stem };
    }
    return { dir: stem.slice(0, slash), base: stem.slice(slash + 1) };
}
// Map a source-shaped path to a prioritized list of candidate test paths.
function candidateTestsFor(path) {
    const parts = extractExtension(path);
    if (parts === undefined) {
        return [];
    }
    const { stem, ext } = parts;
    const { dir, base } = basenameOf(stem);
    const out = [];
    if (stem.startsWith("src/")) {
        out.push(`tests/${stem.slice(4)}.test${ext}`);
    }
    out.push(`${stem}.test${ext}`);
    out.push(`${stem}.spec${ext}`);
    const nestedDir = dir === "" ? "__tests__" : `${dir}/__tests__`;
    out.push(`${nestedDir}/${base}.test${ext}`);
    return out;
}
// Map a test-shaped path back to its paired source path. The marker (".test." / ".spec.")
// and trailing extension are stripped to recover the stem; conventions are inverted to
// produce candidates in priority order.
function candidateSourcesFor(path) {
    const match = TEST_MARKER_RE.exec(path);
    if (match === null) {
        return [];
    }
    const stem = path.slice(0, match.index);
    const extWithDot = match[0].slice(match[0].lastIndexOf("."));
    const { dir, base } = basenameOf(stem);
    const out = [];
    if (stem.startsWith("tests/")) {
        out.push(`src/${stem.slice(6)}${extWithDot}`);
    }
    if (dir.endsWith("/__tests__")) {
        out.push(`${dir.slice(0, -"/__tests__".length)}/${base}${extWithDot}`);
    }
    else if (dir === "__tests__") {
        out.push(`${base}${extWithDot}`);
    }
    out.push(`${stem}${extWithDot}`);
    return Array.from(new Set(out));
}
function isAllowedPath(ctx, relativePath) {
    return ctx.allowedPaths === undefined || ctx.allowedPaths.has(relativePath);
}
function firstExistingPair(ctx, candidates) {
    for (const candidate of candidates) {
        if (!isAllowedPath(ctx, candidate)) {
            continue;
        }
        const abs = resolveWithinWorkspace(ctx.scope.workspace.root, candidate);
        assertContainedRealPath(ctx.fs, ctx.scope.workspace.root, abs, "scope");
        if (ctx.fs.exists(abs)) {
            return candidate;
        }
    }
    return undefined;
}
function emitPairAtom(ctx, pairedPath) {
    return buildAtom({
        scopeId: ctx.scope.scopeId,
        scopePath: pairedPath,
        lineRange: undefined,
        provenanceKind: "structural",
        tool: "test-source-pairing",
        queryFingerprint: ctx.fingerprint,
        score: 0.8,
        emittedAtMs: ctx.nowMs(),
    });
}
function pairForPath(ctx, path) {
    const isTest = TEST_MARKER_RE.test(path);
    const candidates = isTest ? candidateSourcesFor(path) : candidateTestsFor(path);
    const found = firstExistingPair(ctx, candidates);
    return found === undefined ? undefined : emitPairAtom(ctx, found);
}
function pathsForSymbol(ctx, symbol, limits) {
    const lowered = symbol.toLowerCase();
    const out = [];
    const files = ctx.allowedPaths === undefined
        ? gatherCandidates(ctx.scope, limits, ctx.fs).files.map((file) => file.relativePath)
        : [...ctx.allowedPaths];
    for (const relativePath of files) {
        const parts = extractExtension(relativePath);
        if (parts === undefined) {
            continue;
        }
        const base = basenameOf(parts.stem).base.replace(/\.(?:test|spec)$/, "");
        if (base.toLowerCase() === lowered) {
            out.push(relativePath);
        }
    }
    return out;
}
function allowedPathsForScope(scope, limits, fs) {
    if (scope.relativePaths.length === 0) {
        return undefined;
    }
    return new Set(gatherCandidates(scope, limits, fs).files.map((file) => file.relativePath));
}
function inputsForQuery(ctx, query, limits) {
    if (!looksLikePath(query.text)) {
        return pathsForSymbol(ctx, query.text, limits);
    }
    return isAllowedPath(ctx, query.text) ? [query.text] : [];
}
export const testSourcePairingAdapter = {
    name: "test-source-pairing",
    isAvailable: () => Promise.resolve(true),
    lookup: (scope, query, limits, fs, deps) => {
        // Wrap the synchronous body so that throws from assertContainedRealPath surface as a
        // rejected Promise (which the registry runner and `await expect(...).rejects.…` tests
        // can observe). Without this wrapper the throw escapes the call site before the Promise
        // is built.
        try {
            return Promise.resolve(runLookup(scope, query, limits, fs, deps));
        }
        catch (err) {
            return Promise.reject(err instanceof Error ? err : new Error(String(err)));
        }
    },
};
function runLookup(scope, query, limits, fs, deps) {
    if (query.kind !== "natural-language" && query.kind !== "exact-symbol") {
        return [];
    }
    const ctx = {
        scope,
        fs,
        nowMs: deps?.nowMs ?? Date.now,
        fingerprint: queryFingerprint(query),
        allowedPaths: allowedPathsForScope(scope, limits, fs),
    };
    const inputs = inputsForQuery(ctx, query, limits);
    const atoms = [];
    for (const input of inputs) {
        if (atoms.length >= limits.maxMatchesReturned) {
            break;
        }
        const atom = pairForPath(ctx, input);
        if (atom !== undefined) {
            atoms.push(atom);
        }
    }
    return atoms;
}
