import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
import { PromptPlan } from "./promptPlan.js";
import { isFrontendFile } from "./scopeAssessment.js";
import { readDiffFile } from "../../utils/utils.js";
// ── Step label constants ─────────────────────────────────────────────────────
// Derived from the PromptPlan declaration below. Non-conditional steps export
// static string constants (safe to use in tests without passing params).
// The one conditional sub-step (TRACE_CALLERS) exports a function accessor.
//
// Labels:
//   READ_FILES    → "1"    (main step, always present)
//   RESOLVE_PATHS → "1.1"  (sub-step of 1, always present)
//   EXTRACT       → "2"    (main step, always present)
//   TRACE_CALLERS → "2.1"  (sub-step of 2, conditional — use getStepTraceCallers())
//   DRAFT         → "3"    (main step, always present)
//   CALL_TOOL     → "4"    (main step, always present)
// Extensions that indicate a backend code file (has symbols/functions to trace).
// Used to filter unmatchedFiles before emitting the trace-callers sub-step.
const BACKEND_CODE_EXT = /\.(ts|js|mjs|cjs|py|java|kt|rb|go|cs|php|rs|scala|swift|c|cpp|h|hpp)$/i;
// ── Inline note for Java Spring route resolution ──────────────────────────────
// Appended to any step where the LLM reads Java source files. Java Spring has no
// router-mounting file — each controller defines its own class-level prefix, and
// that prefix may reference a constant defined elsewhere.
const JAVA_SPRING_NOTE = `For Java Spring: full URL = class-level \`@RequestMapping\` prefix + method-level path. If the prefix is a constant reference (e.g. \`@RequestMapping(Url.PAGE_URL)\`), find the constant — same file, inner class, or a separate \`Url.java\` — and resolve it (including \`+\` concatenation).`;
const INLINE_DIFF_LIMIT = 12_000;
function _deriveContext(p) {
    const diffFiles = p.parsedDiff?.changedFiles ?? [];
    const preDetectedEndpoints = !!(p.parsedDiff &&
        (p.parsedDiff.newEndpoints.length > 0 ||
            p.parsedDiff.modifiedEndpoints.length > 0 ||
            (p.parsedDiff.removedEndpoints?.length ?? 0) > 0));
    const isUIOnly = diffFiles.length > 0 &&
        !preDetectedEndpoints &&
        diffFiles.every((f) => isFrontendFile(f));
    const canInline = !!(p.diffContent && p.diffContent.length <= INLINE_DIFF_LIMIT);
    return { diffFiles, preDetectedEndpoints, isUIOnly, canInline };
}
// ── Private step title / body helpers ────────────────────────────────────────
// Each function receives AnalysisOutputParams and returns either the step title
// string or the step body text (WITHOUT the "### Step N: Title" header — that is
// added by PromptPlan.render()). Using function declarations so they are hoisted
// and can be referenced in the _plan definition below.
function _readFilesBody(p) {
    const changedFiles = p.parsedDiff?.changedFiles.join(", ") ?? "";
    const { canInline } = _deriveContext(p);
    const diffFileRef = canInline
        ? `\n<diff>\n${p.diffContent}\n</diff>\n`
        : p.diffFilePath
            ? `\n**Full diff file**: \`${p.diffFilePath}\` — **you MUST read this file before proceeding to Step ${ANALYSIS_STEP_EXTRACT}.** It contains the complete unified diff for this PR.\n`
            : "";
    return `${changedFiles}${diffFileRef}`;
}
function _resolvePathsTitle(p) {
    if (p.wsSchemaPath && p.specFetchSucceeded) {
        return "Validate all endpoint paths against the OpenAPI spec";
    }
    if (p.routerMountContext.length) {
        return "Build path resolution table";
    }
    return "Verify endpoint paths from source files";
}
function _resolvePathsBody(p) {
    if (p.wsSchemaPath && p.specFetchSucceeded) {
        return `Fetch \`${p.wsSchemaPath}\` and extract all keys from \`spec.paths\`.
**Before placing any path in a tool call**, confirm it exists in that list.
If a path is NOT in the spec **and it did not come from the PR diff**, find the correct spelling by matching resource name — do NOT use it unverified.
Paths the PR explicitly added or modified may not yet appear in the spec (spec lag) — treat those as valid.`;
    }
    if (p.routerMountContext.length) {
        const hasInlined = (p.routerFileContents?.length ?? 0) > 0;
        return `${hasInlined
            ? "The **Routing entry-point files** section above contains the inlined file contents — use them directly to trace every router mount call"
            : "The **Routing entry-point files** section above lists the files to read.\n\n**Read each of those files** and trace every router mount call"} to understand nesting — the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
- Python (FastAPI/Flask): \`parent.include_router(child, prefix="...")\`, \`app.register_blueprint(...)\`
- JS/TS (Express/Fastify/Hapi): \`app.use('/path', childRouter)\`, \`router.use('/path', sub)\`
- NestJS: \`@Module({ imports: [FeatureModule] })\` — trace the module import chain; each \`@Controller('prefix')\` contributes a segment
- Go (Gin/Echo/Chi): \`r.Group('/path')\`, \`r.Mount('/path', sub)\`
- Ruby (Rails): \`namespace\`, \`scope\`, \`resources ... do\`
- Django: \`path('prefix/', include(urls))\`

Chain all segments from the app root down through every intermediate mount to each leaf router file. Build a table:

| Source file | Full URL prefix |
|-------------|----------------|
| (leaf router file) | (fully chained prefix, e.g. /api/v1/products/{id}/reviews) |

**This table is authoritative.** Before placing any URL in a tool call, look up the source file. If the pre-built catalog shows a different path, use the table value.`;
    }
    const specFailedNote = p.wsSchemaPath && !p.specFetchSucceeded
        ? `\n> ⚠️ A spec was configured (\`${p.wsSchemaPath}\`) but could not be loaded at analysis time — treat all paths as unverified until confirmed against source.`
        : "";
    return `The endpoint catalog below was produced by static regex analysis and is **unverified**.
Before using any path in a tool call, read the route definition file identified in the "Source" column and confirm the path string exactly.
Pay special attention to mount prefixes — a router at \`/api/v1\` + route \`/version\` → path is \`/api/v1/version\`, not \`/api/server-version\`.${specFailedNote}`;
}
function _extractTitle(p) {
    const { isUIOnly, canInline } = _deriveContext(p);
    if (isUIOnly)
        return "Identify consumed API endpoints and integration status";
    if (canInline || p.diffFilePath)
        return "Extract new, modified, and removed API endpoints from the diff";
    return "Extract new, modified, and removed API endpoints from source files";
}
function _extractBody(p) {
    const { diffFiles, isUIOnly, canInline, preDetectedEndpoints } = _deriveContext(p);
    const diffHasJavaFiles = diffFiles.some((f) => /\.(java|kt)$/.test(f));
    if (isUIOnly) {
        return `UI-only PR — perform two checks:
1. Read changed frontend files to find API calls (fetch, axios, hooks).
2. For each changed component file (skip CSS/HTML/style-only files — they have no exported component name to search for): check whether any production source file imports, re-exports, or renders it.
   - Search for both the component's exported name AND its module path/filename to catch aliased and default imports (e.g. \`import Foo from './CartLine'\`).
   - Derive the exported name from the file itself: use the default export name, a named exported PascalCase component, or the PascalCase file basename when no clearer name exists.
   - Exclude test/story files from the search: ignore matches in \`*.test.*\`, \`*.spec.*\`, \`*.stories.*\`, and \`__tests__/\` directories — only production code imports count as integration.

If no production file imports, re-exports, or renders a changed component, mark it as **unintegrated** in the Execution Plan output.
Exception: if the same PR also adds a route/page file (e.g. under Next.js \`pages/\` or \`app/\`) that imports the component, the route IS the integration point — do NOT mark it as unintegrated.
Do NOT apply the unintegrated heuristic to route/entrypoint files themselves — those are always reachable by convention.
An unintegrated non-route component has no DOM node in the running app and cannot be browser-tested — it qualifies as a dead-code / unintegrated-component no-surface PR regardless of how complex the component logic is.

**While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
- **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations → _data_integrity_
- **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
- **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
- **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
    }
    if (canInline || p.diffFilePath) {
        return `${canInline ? "Read the \`<diff>\` above" : `Read the diff file at \`${p.diffFilePath}\``} and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` → full path \`/api/v1/products/{product_id}/reviews\`).
${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
For each endpoint found: note the HTTP method, full path, and source file.
${preDetectedEndpoints ? "The endpoint catalog above already lists some changed endpoints — verify and augment with anything it missed." : "No endpoints were pre-detected in the changed files — extract them from the diff."}
**Also identify removed endpoints**: Look for deleted route annotations (lines starting with \`-\` in the diff) in modified files (files that still exist but had routes deleted). A removed endpoint is a route definition present in the base branch but absent in the current branch. Cross-reference against the scanned endpoint listing below — if a deleted route annotation's endpoint still appears there (e.g. moved to another file), it is NOT removed. Only flag endpoints that are truly gone from the codebase.
**CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
identify which parameters are URL query params vs request body. Look at framework-specific
annotations (FastAPI \`Query()\`, Express \`req.query\`, Spring \`@RequestParam\`, etc.).
Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.

**While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
- **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations → _data_integrity_
- **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
- **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
- **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
    }
    return `No diff was available — read the changed source files listed above directly to identify new or modified API endpoints. Use the **Router Mounting / Nesting** section to reconstruct full paths.
${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
For each endpoint found: note the HTTP method, full path, and source file.
Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.

**While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
- **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations → _data_integrity_
- **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
- **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
- **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
}
function _traceCallersCondition(p) {
    // Only emit in diff scope — the plan may be preview()'d with full-repo params.
    const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
    const { isUIOnly } = _deriveContext(p);
    const traceableUnmatched = (p.unmatchedFiles ?? []).filter((f) => BACKEND_CODE_EXT.test(f));
    return isDiffScope && !isUIOnly && traceableUnmatched.length > 0;
}
function _traceCallersBody(p) {
    const traceableUnmatched = (p.unmatchedFiles ?? []).filter((f) => BACKEND_CODE_EXT.test(f));
    return `The following changed files contain **no HTTP endpoint registrations** (no route annotations, controller mappings, or handler decorators). Their changes will only be tested if you find and target the HTTP endpoints that *call* them:

${traceableUnmatched.map((f) => `- \`${f}\``).join("\n")}

For each file above:
1. **Find the changed symbols** — read the diff (or the file) to identify which functions, methods, or classes were modified.
2. **Search for callers** — look for import statements and call sites of those symbols across service, handler, and controller files. Use fully qualified names (e.g. \`DataUtils.addFileData\`, not just \`addFileData\`) to avoid false matches in large monorepos.
3. **Trace to HTTP registration** — from each caller, follow up to the route/controller registration (Spring \`@PostMapping\`, Express \`router.post\`, FastAPI \`@router.post\`, etc.) to identify the endpoint(s) that invoke the changed logic.
4. **Augment the endpoint list** from Step ${ANALYSIS_STEP_EXTRACT} with these execution-path endpoints.
5. If an execution or processing endpoint is found (path ending in \`/execute\`, \`/run\`, \`/trigger\`, \`/process\`, \`/invoke\`, or similar), it **MUST** be included in the test candidates. Do not produce coverage consisting solely of CRUD endpoints when an execution-path endpoint was found — CRUD tests may still be included but must not be the only coverage.`;
}
function _draftTitle(p) {
    return p.nextTool === "skyramp_analyze_test_health"
        ? "Identify tests at risk of drift"
        : "Draft integration scenarios";
}
function _draftBody(p) {
    if (p.nextTool === "skyramp_analyze_test_health") {
        return "Assess which existing tests may be broken by the changes in this diff.";
    }
    return `Draft multi-step scenarios simulating realistic user workflows:
- **Cross-resource data flow**: Foreign key relationships, parent→child creation, verification
- **Search/filter verification**: Create data, search for it using \`queryParams\`, verify results
- **Negative/error paths**: Invalid references → appropriate errors
- **UI user journeys**: Concrete browser steps for frontend changes

**Quality:** Realistic request bodies for POST/PUT/PATCH, \`queryParams\` for GET search/filter,
response data verification, actual field names for chaining.
**Parameter placement:** GET search/filter endpoints MUST use \`queryParams\`, not \`requestBody\`.

**No duplicate scenarios.** Each scenario must cover a distinct code path (unique method + path + expected status). Do NOT draft two scenarios that differ only in request body values but hit the same code path (e.g. discount=10% vs discount=25% — both succeed with 200, same logic). A negative-case variant with a different expected status (e.g. discount=-10% → 422) IS a distinct scenario — use a single-step contract test for it (see below).

**For each new or modified endpoint, ensure at least one error-path scenario is drafted** — a single-step contract test that triggers a specific error (404 for a missing resource ID, 422 for an invalid field value) that the source code explicitly handles. One auth-boundary scenario (missing auth → 401/403) is enough across all endpoints — do not repeat it per endpoint.

**For every scenario you draft, fill \`bugCatchingTarget\`** with the specific formula, constraint, or failure mode the test is designed to expose. Examples:
- \`"discount formula: total_amount = subtotal * (1 - discount_value / 100) — wrong if addition is used instead of subtraction"\`
- \`"items not recalculated after PATCH — total_amount stays at old value if collection update is ignored"\`
- \`"missing 404 guard on resource ID — returns 500 instead of 404 for unknown IDs"\`
This field is used at test generation time to compute exact assertion values. Leave it empty only if no specific formula or constraint applies.`;
}
function _callToolTitle(p) {
    return p.nextTool === "skyramp_analyze_test_health"
        ? "Call analyze test health"
        : "Call recommend tests";
}
function _callToolBody(p) {
    if (p.nextTool === "skyramp_analyze_test_health") {
        return `Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``;
    }
    return `Call \`skyramp_recommend_tests\` with:
- \`stateFile: "${p.stateFile}"\`
- \`enrichedScenarios\`: (optional) JSON array of your Step ${ANALYSIS_STEP_DRAFT} scenarios — see the tool's inputSchema for the exact shape. Your enriched scenarios override server-side ones with the same \`scenarioName\` and are prioritized in ranking. Omit if you drafted nothing in Step ${ANALYSIS_STEP_DRAFT}.`;
}
// ── PromptPlan declaration ─────────────────────────────────────────────────────
// Defines the diff-scope prompt structure: phases, steps, sub-steps, and conditions.
// Used to:
//   1. Derive the exported step label constants below.
//   2. Render the diff-scope prompt via _plan.render(p).
//   3. Enable developer review via _plan.preview(p).
const _plan = new PromptPlan()
    .addPhase("analysis", "Your Task — Enrich & Recommend (PR-scoped)", {
    headerLevel: "##",
    stepFormat: "hash",
})
    .step("READ_FILES", "Read the changed files and diff", _readFilesBody)
    .subStep("RESOLVE_PATHS", _resolvePathsTitle, _resolvePathsBody)
    .step("EXTRACT", _extractTitle, _extractBody)
    .subStep("TRACE_CALLERS", "Trace callers of changed non-route files", _traceCallersBody, {
    when: _traceCallersCondition,
    whenDesc: "non-frontend unmatched backend files present in diff scope",
})
    .step("DRAFT", _draftTitle, _draftBody)
    .step("CALL_TOOL", _callToolTitle, _callToolBody)
    .done();
// ── Exported step label constants ─────────────────────────────────────────────
/** "1" — Read source files / changed files */
export const ANALYSIS_STEP_READ_FILES = _plan.labels.READ_FILES; // "1"
/** "1.1" — Verify/build/validate endpoint path resolution table */
export const ANALYSIS_STEP_RESOLVE_PATHS = _plan.labels.RESOLVE_PATHS; // "1.1"
/** "2" — Extract or map endpoints */
export const ANALYSIS_STEP_EXTRACT = _plan.labels.EXTRACT; // "2"
/** "3" — Draft scenarios / present catalog / identify drift */
export const ANALYSIS_STEP_DRAFT = _plan.labels.DRAFT; // "3"
/** "4" — Call the downstream tool (recommend_tests / analyze_test_health) */
export const ANALYSIS_STEP_CALL_TOOL = _plan.labels.CALL_TOOL; // "4"
/**
 * Returns the step label for the trace-callers sub-step given runtime params.
 * Returns "2.1" when the step is active (non-frontend unmatched backend files
 * present in diff scope), or null when the step is absent.
 *
 * Use in tests instead of hardcoding "2.1":
 *   const label = getStepTraceCallers(params);
 *   if (label) expect(output).toContain(`### Step ${label}: Trace callers`);
 *   else expect(output).not.toContain("Trace callers");
 */
export function getStepTraceCallers(p) {
    return _plan.labelFor("TRACE_CALLERS", p);
}
/**
 * Returns the full "### Step N.N: Title\nbody" block for the path resolution
 * sub-step. Used by the full-repo scope path which builds its prompt manually
 * rather than calling _plan.render().
 */
function buildPathResolutionTableSection(p) {
    const label = ANALYSIS_STEP_RESOLVE_PATHS;
    const title = _resolvePathsTitle(p);
    const body = _resolvePathsBody(p);
    return `### Step ${label}: ${title}\n${body}\n`;
}
function buildEnrichmentInstructions(p) {
    const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
    const useHealthFlow = p.nextTool === "skyramp_analyze_test_health";
    if (!isDiffScope) {
        const nextStep = useHealthFlow
            ? `### Step ${ANALYSIS_STEP_DRAFT}: Identify tests at risk of drift
Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``
            : `### Step ${ANALYSIS_STEP_DRAFT}: Present the catalog
The ranked test recommendation catalog is pre-built and shown below (after the separator line).

**Your only job is to present it.**

1. Fill in every \`<…from source>\` placeholder using the field names, computed formulas, and auth details you found in Steps 1–2.
2. Output the completed catalog **exactly as formatted — grouped by test type (### E2E / ### UI / ### Integration / ### Contract)**. Do NOT restructure, reorder, rename sections, or generate a new format.
3. Do NOT call any Skyramp generation tools. The catalog shows ready-to-use tool calls that can be executed on demand.

**If** Steps 1–2 revealed additional scenarios the catalog does not cover (e.g. a computed formula or FK relationship that was missed), you may optionally call \`skyramp_recommend_tests\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\` and \`enrichedScenarios\` to regenerate a more complete catalog — but only after presenting the current one.`;
        const hasJavaFiles = p.candidateRouteFiles?.some((f) => /\.(java|kt)$/.test(f)) ?? false;
        const routeFilesSection = p.candidateRouteFiles && p.candidateRouteFiles.length > 0
            ? `\nRoute/controller files found by static scan (read these to discover endpoints — the regex-based catalog below may be incomplete for your framework):\n${p.candidateRouteFiles.map((f) => `- ${f}`).join("\n")}\n`
            : "";
        const resolvePathsNote = p.routerMountContext.length
            ? `**Resolve nested paths** using your Step ${ANALYSIS_STEP_RESOLVE_PATHS} table — a router in the table with prefix \`/api/v1/products/{product_id}/reviews\` means every endpoint in that file lives under that full path.`
            : `**Resolve full paths** using the prefixes you identified in Step ${ANALYSIS_STEP_READ_FILES} (e.g. Java Spring class-level \`@RequestMapping\` prefix + method-level path).`;
        return `## Your Task — Fill in and Present the Catalog (full repo)

### Step ${ANALYSIS_STEP_READ_FILES}: Read key files
${routeFilesSection}Read the route/controller files above **and** model/schema files (Pydantic models, Zod schemas, DTOs) to find: required request body fields, computed response fields and formulas, auth middleware type, storage backend, and how sub-routers are mounted (cross-check against Router Mounting section above).
${hasJavaFiles ? JAVA_SPRING_NOTE : ""}
If the endpoint catalog below is missing endpoints visible in these files (e.g. from a framework the static scanner doesn't recognise), extract them now${useHealthFlow ? "." : " and include them in Step ${ANALYSIS_STEP_DRAFT}'s `enrichedScenarios`."}

${buildPathResolutionTableSection(p)}### Step ${ANALYSIS_STEP_EXTRACT}: Map cross-resource relationships and resolve endpoint paths
(Distinct from Step ${ANALYSIS_STEP_READ_FILES} — Step ${ANALYSIS_STEP_READ_FILES} reads individual schemas; Step ${ANALYSIS_STEP_EXTRACT} maps how endpoints relate to each other.)
For each endpoint: which POST creates resources consumed by other endpoints?
${resolvePathsNote}
For GET list endpoints: identify query params (\`limit\`, \`offset\`, \`order\`, \`orderBy\`) from framework annotations (FastAPI \`Query()\`, Express \`req.query\`, etc.).

${nextStep}`;
    }
    // Diff scope — delegate entirely to the plan.
    return _plan.render(p);
}
export function buildAnalysisOutputText(p) {
    // Centralize diff reading here — resolve content from file once so helpers don't each read it.
    p = { ...p, diffContent: readDiffFile(p.diffFilePath) };
    const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
    // Router mounting context is unique to this prompt; shown whenever mount context
    // is available, regardless of whether a spec is configured.
    const routerSection = p.routerMountContext.length
        ? `
## Routing entry-point files
${p.routerFileContents?.length
            ? p.routerFileContents
                .map(({ file, content }) => `### \`${file}\`\n\`\`\`\n${content}\n\`\`\``)
                .join("\n\n") +
                (p.routerMountContext.length > (p.routerFileContents?.length ?? 0)
                    ? `\n\nAdditional files (too large to inline — read manually if needed):\n` +
                        p.routerMountContext
                            .filter((f) => !(p.routerFileContents ?? []).some((r) => r.file === f))
                            .map((f) => `- \`${f}\``)
                            .join("\n")
                    : "")
            : `Read these in Step ${ANALYSIS_STEP_RESOLVE_PATHS} to trace the full router/module hierarchy:\n` +
                p.routerMountContext.map((f) => `- \`${f}\``).join("\n")}`
        : "";
    const enrichment = buildEnrichmentInstructions(p);
    // LLM fallback: when heuristic scanning may have missed backend route files.
    const scannerFallbackHasCatalog = p.scannedEndpoints.length > 0;
    const scannerFallbackReason = scannerFallbackHasCatalog
        ? "The heuristic endpoint scanner may have missed route/controller files in this diff, even though endpoint data is present from another source"
        : "The heuristic endpoint scanner found **0 endpoints**";
    const scannerFallbackInstruction = scannerFallbackHasCatalog
        ? "Treat this as a supplemental gap check: merge only HTTP endpoints that are visibly missing from the existing catalog/spec data, and keep the existing catalog as the primary endpoint source."
        : "Build a table of discovered endpoints (method, full path, source file) and use it as the authoritative endpoint list for all subsequent steps.";
    const llmFallbackSection = (isDiffScope || p.scannedEndpoints.length === 0) &&
        p.candidateRouteFiles &&
        p.candidateRouteFiles.length > 0
        ? `
## Scanner Fallback — Manual Endpoint Discovery Required

${scannerFallbackReason}, and this repository contains ${p.candidateRouteFiles.length} file(s) that appear to define HTTP routes. The scanner likely failed due to multi-line definitions, framework-specific patterns, indirection the regex does not cover, or endpoint data being supplied by an OpenAPI spec instead of source scanning.

**Read the following controller/router files and identify all HTTP route registrations (method + path).** Include these discovered endpoints when executing the steps above.

${p.candidateRouteFiles.slice(0, 15).map((f) => `- \`${f}\``).join("\n")}
${p.candidateRouteFiles.length > 15 ? `\n_(${p.candidateRouteFiles.length - 15} more files not shown)_` : ""}

For each file, look for:
- Express/Fastify/Koa/Hapi: \`router.<method>('/path', ...)\` or \`app.<method>('/path', ...)\`
- FastAPI/Flask: \`@router.<method>('/path')\` or \`@app.route('/path', ...)\`
- Spring/NestJS: \`@GetMapping\`, \`@PostMapping\`, \`@Controller\`, etc.
- Go (Gin/Echo/Chi): \`r.GET("/path", ...)\`, \`r.Group("/prefix")\`
- GraphQL: schema/resolver artifacts are unsupported for REST test generation; do not invent REST endpoints from them
- Any other framework-specific route registration pattern

${scannerFallbackInstruction}`
        : "";
    return `# Repository Analysis

**Session ID**: \`${p.sessionId}\`
**Repository**: \`${p.repositoryPath}\`
**Analysis Scope**: \`${p.analysisScope}\`
${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0) + (p.parsedDiff?.removedEndpoints?.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
${routerSection}
${llmFallbackSection}
${enrichment}

**CRITICAL**: No .json/.md file creation. Prioritize cross-resource workflows.`;
}
