// Workspace detection: walk up from a start directory to the nearest root containing a
// `.git` entry or a `package.json`, then read safe metadata. The only JSON parse in the
// module is wrapped in a single try/catch at this IO boundary (ADR-0005). No clock, no RNG.
import { dirname, join, relative, resolve } from "node:path";
import { nodeWorkspaceFs } from "./fs.js";
import { WorkspaceNotFoundError } from "./errors.js";
import { isDenied } from "./ignore.js";
import { assertContainedRealPath } from "./realpath.js";
const MARKERS = [".git", "package.json"];
function isRoot(dir, fs) {
    return MARKERS.some((marker) => fs.exists(join(dir, marker)));
}
function findRoot(startDir, fs) {
    let current = resolve(startDir);
    // Bounded by the filesystem: dirname() reaches a fixed point at the volume root.
    for (;;) {
        if (isRoot(current, fs)) {
            return current;
        }
        const parent = dirname(current);
        if (parent === current) {
            throw new WorkspaceNotFoundError(`no workspace root (.git or package.json) found above ${startDir}`, startDir);
        }
        current = parent;
    }
}
function asString(value) {
    return typeof value === "string" ? value : undefined;
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function depKeys(value) {
    return isRecord(value) ? Object.keys(value) : [];
}
function detectFramework(record) {
    const names = new Set([
        ...depKeys(record.devDependencies),
        ...depKeys(record.dependencies),
    ]);
    if (names.has("vitest")) {
        return "vitest";
    }
    if (names.has("jest")) {
        return "jest";
    }
    if (names.has("mocha")) {
        return "mocha";
    }
    return "unknown";
}
const EMPTY_META = { name: undefined, version: undefined, testFramework: "unknown" };
function toRelative(root, absolutePath) {
    return relative(root, absolutePath).split("\\").join("/");
}
function toRealRelative(root, fs, absolutePath) {
    try {
        return toRelative(fs.realPath(root), absolutePath);
    }
    catch {
        return toRelative(root, absolutePath);
    }
}
function readContainedText(root, path, fs) {
    if (!fs.exists(path)) {
        return undefined;
    }
    try {
        const containedPath = assertContainedRealPath(fs, root, path, path);
        if (isDenied(toRealRelative(root, fs, containedPath))) {
            return undefined;
        }
        return fs.readFileUtf8(containedPath);
    }
    catch {
        return undefined;
    }
}
function readPackageMeta(root, fs) {
    const path = join(root, "package.json");
    try {
        const raw = readContainedText(root, path, fs);
        if (raw === undefined) {
            return EMPTY_META;
        }
        const parsed = JSON.parse(raw);
        if (!isRecord(parsed)) {
            return EMPTY_META;
        }
        return {
            name: asString(parsed.name),
            version: asString(parsed.version),
            testFramework: detectFramework(parsed),
        };
    }
    catch {
        return EMPTY_META;
    }
}
function readIgnoreLines(root, fs) {
    const path = join(root, ".gitignore");
    const raw = readContainedText(root, path, fs);
    if (raw === undefined) {
        return [];
    }
    return raw.split(/\r?\n/);
}
function isExistingDir(absolutePath, fs) {
    return fs.exists(absolutePath) && fs.stat(absolutePath).isDirectory;
}
function detectDirs(root, fs, candidates) {
    return candidates.filter((dir) => isExistingDir(join(root, dir), fs));
}
function detectLanguages(root, fs) {
    const languages = [];
    if (fs.exists(join(root, "tsconfig.json"))) {
        languages.push("typescript");
    }
    languages.push("javascript");
    return languages;
}
// Build workspace metadata treating `root` as THE workspace root — no walk-up, never throws
// WorkspaceNotFoundError. The connected-context feature uses this for a folder the user explicitly
// connected (Files-window scope): that folder IS the root even when it carries no `.git`/
// `package.json` marker, so an arbitrary documents folder is searchable just like a repository.
export function detectWorkspaceAt(root, fs = nodeWorkspaceFs) {
    const resolved = resolve(root);
    const meta = readPackageMeta(resolved, fs);
    return {
        root: resolved,
        name: meta.name,
        version: meta.version,
        testFramework: meta.testFramework,
        sourceDirs: detectDirs(resolved, fs, ["src"]),
        testDirs: detectDirs(resolved, fs, ["tests", "test", "__tests__"]),
        languages: detectLanguages(resolved, fs),
        ignoreLines: readIgnoreLines(resolved, fs),
    };
}
// Walk up from startDir to the nearest `.git`/`package.json` root, then read its metadata. Used
// for auto-detection (e.g. the CLI) where the caller starts inside a repository subdirectory.
export function detectWorkspace(startDir, fs = nodeWorkspaceFs) {
    return detectWorkspaceAt(findRoot(startDir, fs), fs);
}
