/**
 * Helpers for keeping apiTestingState in sync when SDK APIs are created or edited
 * via file tools (build_writeFile, build_editFile, build_multiEditFile).
 *
 * Mirrors how sdk-api-discovery.ts in the library iframe discovers APIs by
 * reading server/apis/index.ts as the single source of truth. The parsed
 * registry is cached on clark.context.sdkApiRegistryEntryPoints so we only
 * re-parse when the registry file itself changes.
 */
import { parseSdkRegistryEntryPoints, SDK_REGISTRY_PATH, } from "./sdk-registry.js";
/**
 * Ensure the Clark context has a cached copy of the SDK API registry.
 * Lazily parses server/apis/index.ts on first access; subsequent calls
 * return the cached map until the registry file is explicitly re-parsed.
 */
async function ensureRegistryCache(clark, services) {
    if (clark.context.sdkApiRegistryEntryPoints) {
        return clark.context.sdkApiRegistryEntryPoints;
    }
    const pathExists = (p) => services.appShell.pathExists(p);
    let content = "";
    try {
        if (await pathExists(SDK_REGISTRY_PATH)) {
            content = await services.appShell.readFile(SDK_REGISTRY_PATH);
        }
    }
    catch {
        // Registry doesn't exist yet — empty map is fine
    }
    const entryPoints = await parseSdkRegistryEntryPoints(content, pathExists);
    clark.updateContext({ sdkApiRegistryEntryPoints: entryPoints });
    return entryPoints;
}
/**
 * Marks affected SDK APIs as untested so build_debug and build_finalize will
 * require testApi before finalizing.
 *
 * Uses the cached SDK API registry (server/apis/index.ts) as the source of
 * truth — the same file that sdk-api-discovery.ts uses in the library iframe.
 *
 * Behaviour:
 * - Editing server/apis/index.ts: re-parses the new content, diffs against
 *   the cached version, and marks only newly added / changed APIs.
 * - Editing a registered entry-point file: marks only the APIs that use it.
 * - Editing a file scoped to an API's directory subtree (e.g. files under
 *   server/apis/my-api/ when the entry point is server/apis/my-api.ts or
 *   server/apis/my-api/index.ts): marks only the owning API(s).
 * - All other files (shared modules, files outside server/): no-op — the
 *   agent is not forced to re-test unrelated APIs.
 */
export async function markSdkApiAsUntestedIfNeeded(filePath, clark, services) {
    if (!services.features?.sdkApiEnabled) {
        return;
    }
    let previousEntryPoints;
    if (filePath === SDK_REGISTRY_PATH &&
        !clark.context.sdkApiRegistryEntryPoints) {
        // Registry edits are tracked after writes; if no cache exists yet, disk already
        // reflects the new content, so treat the previous registry as empty.
        previousEntryPoints = new Map();
    }
    else {
        previousEntryPoints = await ensureRegistryCache(clark, services);
    }
    if (filePath === SDK_REGISTRY_PATH) {
        const pathExists = (p) => services.appShell.pathExists(p);
        let newContent = "";
        try {
            newContent = await services.appShell.readFile(SDK_REGISTRY_PATH);
        }
        catch {
            // file was deleted — treat as empty
        }
        const newEntryPoints = await parseSdkRegistryEntryPoints(newContent, pathExists);
        // Update the cache to the new state
        clark.updateContext({ sdkApiRegistryEntryPoints: newEntryPoints });
        // Only mark APIs that were added or had their entry point changed
        const apiNamesToMark = Array.from(newEntryPoints.entries())
            .filter(([name, ep]) => previousEntryPoints.get(name) !== ep)
            .map(([name]) => name);
        // Remove APIs that were deleted from the registry from all testing-state sets.
        const apiNamesToRemove = Array.from(previousEntryPoints.keys()).filter((name) => !newEntryPoints.has(name));
        removeApis(clark, apiNamesToRemove);
        // Registry-level changes (new API or changed entry point) are meaningful
        // structural changes — clear any prior denial so the API is re-tested.
        markApis(clark, apiNamesToMark, { clearDenied: true });
        return;
    }
    if (previousEntryPoints.size === 0) {
        return;
    }
    // Check if the edited file is a registered entry point
    const directlyAffected = Array.from(previousEntryPoints.entries())
        .filter(([, ep]) => ep === filePath)
        .map(([name]) => name);
    if (directlyAffected.length > 0) {
        markApis(clark, directlyAffected);
        return;
    }
    // Check if the file lives inside an API's directory subtree.
    // Derives a scope prefix from each entry point:
    //   server/apis/foo.ts       → server/apis/foo/
    //   server/apis/bar/index.ts → server/apis/bar/
    const scopedApis = Array.from(previousEntryPoints.entries())
        .filter(([, ep]) => filePath.startsWith(entryPointScope(ep)))
        .map(([name]) => name);
    if (scopedApis.length > 0) {
        markApis(clark, scopedApis);
    }
}
/**
 * Derives a directory scope from an entry-point path so that sibling files
 * can be attributed to the same API.
 *
 *   server/apis/foo.ts       → server/apis/foo/
 *   server/apis/bar/index.ts → server/apis/bar/
 */
function entryPointScope(entryPoint) {
    const withoutExt = entryPoint.replace(/\.(ts|tsx|js)$/, "");
    if (withoutExt.endsWith("/index")) {
        return withoutExt.slice(0, -"index".length);
    }
    return withoutExt + "/";
}
function markApis(clark, apiNames, options) {
    if (apiNames.length === 0) {
        return;
    }
    clark.updateContext((ctx) => {
        const untested = new Set(ctx.apiTestingState?.untested ?? []);
        const failed = new Set(ctx.apiTestingState?.failed ?? []);
        const passed = new Set(ctx.apiTestingState?.passed ?? []);
        const denied = new Set(ctx.apiTestingState?.denied ?? []);
        for (const apiName of apiNames) {
            if (options?.clearDenied) {
                // Registry-level changes (new API or changed entry point) are
                // structural — clear denial so the API can be re-tested.
                denied.delete(apiName);
            }
            else if (denied.has(apiName)) {
                // APPS-2717: Skip APIs whose test was already denied by the user.
                // Re-adding them to `untested` after a file rewrite creates an
                // infinite loop: deny → rewrite → untested → build_debug blocks →
                // testApi → deny → …
                continue;
            }
            untested.add(apiName);
            failed.delete(apiName);
            passed.delete(apiName);
        }
        return {
            ...ctx,
            apiTestingState: { untested, failed, passed, denied },
        };
    });
}
function removeApis(clark, apiNames) {
    if (apiNames.length === 0) {
        return;
    }
    clark.updateContext((ctx) => {
        const untested = new Set(ctx.apiTestingState?.untested ?? []);
        const failed = new Set(ctx.apiTestingState?.failed ?? []);
        const passed = new Set(ctx.apiTestingState?.passed ?? []);
        const denied = new Set(ctx.apiTestingState?.denied ?? []);
        for (const apiName of apiNames) {
            untested.delete(apiName);
            failed.delete(apiName);
            passed.delete(apiName);
            denied.delete(apiName);
        }
        return {
            ...ctx,
            apiTestingState: { untested, failed, passed, denied },
        };
    });
}
//# sourceMappingURL=api-testing-state.js.map