/**
 * Route Tree Test Utilities
 *
 * Builds a route tree from urls() patterns and exposes the manifest,
 * patterns map, and route entries for inspection. Replicates the
 * pattern extraction logic from createRouter.routes() (router.ts)
 * without importing the full router (which depends on virtual modules).
 *
 * Not shipped with the npm package (lives in __tests__/).
 */
import React from "react";
import { RSCRouterContext, runWithPrefixes, } from "../../server/context.js";
import { findMatch, isLazyEvaluationNeeded, } from "../../router/pattern-matching.js";
// ============================================================================
// buildRouteTree
// ============================================================================
/**
 * Build a route tree from url patterns for inspection.
 *
 * Executes the urls() handler inside RSCRouterContext and extracts
 * the manifest, patterns, and route entries. Lazy includes are
 * eagerly evaluated so the full tree is available.
 *
 * @example
 * ```ts
 * const tree = buildRouteTree(
 *   urls(({ path, layout }) => [
 *     layout(RootLayout, () => [
 *       path("/", HomePage, { name: "home" }),
 *       path("/about", AboutPage, { name: "about" }),
 *     ]),
 *   ])
 * );
 *
 * // Inspect patterns
 * expect(tree.routes()).toEqual({ home: "/", about: "/about" });
 *
 * // Inspect segment IDs
 * expect(tree.segmentId("home")).toBe("M0L0L0R0");
 *
 * // Match a URL
 * const match = tree.match("/about");
 * expect(match?.routeKey).toBe("about");
 * ```
 */
export function buildRouteTree(urlPatterns, options) {
    const manifest = new Map();
    const patterns = new Map();
    const patternsByPrefix = new Map();
    const trailingSlashMap = new Map();
    const mountIndex = options?.mountIndex ?? 0;
    // Synthetic root layout to match createRouter behavior
    const syntheticMapRoot = {
        type: "layout",
        id: `#synthetic-maproot-M${mountIndex}`,
        shortCode: `M${mountIndex}L0`,
        parent: null,
        handler: React.createElement(React.Fragment),
        middleware: [],
        revalidate: [],
        errorBoundary: [],
        notFoundBoundary: [],
        layout: [],
        parallel: [],
        intercept: [],
        loader: [],
    };
    let handlerResult = [];
    RSCRouterContext.run({
        manifest,
        patterns,
        patternsByPrefix,
        trailingSlash: trailingSlashMap,
        namespace: "root",
        parent: syntheticMapRoot,
        counters: {},
        mountIndex,
    }, () => {
        handlerResult = urlPatterns.handler();
    });
    // Eagerly evaluate lazy includes
    const lazyIncludes = findLazyIncludes(handlerResult);
    for (const lazy of lazyIncludes) {
        const lazyManifest = new Map();
        const lazyPatterns = new Map();
        const lazyCounters = {};
        if (lazy.context.counters) {
            for (const [key, value] of Object.entries(lazy.context.counters)) {
                lazyCounters[key] = value;
            }
        }
        RSCRouterContext.run({
            manifest: lazyManifest,
            patterns: lazyPatterns,
            patternsByPrefix: new Map(),
            trailingSlash: trailingSlashMap,
            namespace: "lazy",
            parent: lazy.context.parent ?? null,
            counters: lazyCounters,
            mountIndex,
        }, () => {
            const fullPrefix = (lazy.context.urlPrefix || "") + lazy.prefix;
            if (fullPrefix || lazy.context.namePrefix) {
                runWithPrefixes(fullPrefix, lazy.context.namePrefix, () => {
                    lazy.patterns.handler();
                });
            }
            else {
                lazy.patterns.handler();
            }
        });
        for (const [k, v] of lazyManifest)
            manifest.set(k, v);
        for (const [k, v] of lazyPatterns)
            patterns.set(k, v);
    }
    // Build route entries for findMatch
    const routesObject = {};
    for (const [name, pattern] of patterns.entries()) {
        routesObject[name] = pattern;
    }
    const routeEntries = [
        {
            prefix: "",
            staticPrefix: "",
            routes: routesObject,
            handler: urlPatterns.handler,
            mountIndex,
        },
    ];
    return new RouteTree(manifest, patterns, routeEntries);
}
// ============================================================================
// RouteTree
// ============================================================================
/**
 * Wrapper around the raw manifest/patterns/entries providing
 * convenient inspection methods for route tree, segment IDs,
 * middleware, intercepts, loaders, and parallel slots.
 */
export class RouteTree {
    constructor(manifest, patterns, routeEntries) {
        this.manifest = manifest;
        this.patterns = patterns;
        this.routeEntries = routeEntries;
    }
    // --- Route patterns ---
    /** Route name -> URL pattern map as a plain object */
    routes() {
        return Object.fromEntries(this.patterns);
    }
    /** Get all route names */
    routeNames() {
        return [...this.patterns.keys()];
    }
    // --- Segment IDs ---
    /** Get the segment ID (shortCode) for a named route */
    segmentId(name) {
        return this.entry(name)?.shortCode;
    }
    /** Get segment IDs for all named routes */
    segmentIds() {
        const result = {};
        for (const name of this.patterns.keys()) {
            const id = this.segmentId(name);
            if (id)
                result[name] = id;
        }
        return result;
    }
    /**
     * Get the full segment path from root to a named route.
     * Returns an array of { id, type, pattern? } entries.
     */
    segmentPath(name) {
        const entry = this.entry(name);
        if (!entry)
            return [];
        const path = [];
        let current = entry;
        while (current) {
            path.unshift({
                id: current.shortCode,
                type: current.type,
                pattern: current.type === "route" ? current.pattern : undefined,
            });
            current = current.parent;
        }
        return path;
    }
    // --- Entry access ---
    /** Get the EntryData for a named route */
    entry(name) {
        const direct = this.manifest.get(name);
        if (direct)
            return direct;
        for (const [key, entry] of this.manifest) {
            if (key.endsWith(`.${name}`))
                return entry;
        }
        return undefined;
    }
    /** Get the EntryData for a route matching a URL pattern string */
    entryByPattern(pattern) {
        for (const entry of this.manifest.values()) {
            if (entry.type === "route" && entry.pattern === pattern) {
                return entry;
            }
        }
        return undefined;
    }
    // --- URL matching ---
    /** Match a pathname and return the match result (routeKey, params) */
    match(pathname) {
        const result = findMatch(pathname, this.routeEntries);
        if (!result || isLazyEvaluationNeeded(result))
            return null;
        return result;
    }
    // --- Middleware ---
    /** Get middleware functions attached to a named route/layout */
    middleware(name) {
        const entry = this.entry(name);
        return entry?.middleware ?? [];
    }
    /** Check if a named route/layout has middleware */
    hasMiddleware(name) {
        return this.middleware(name).length > 0;
    }
    /**
     * Collect all middleware in the chain from root to a named route.
     * Returns them in execution order (root first).
     */
    middlewareChain(name) {
        const entry = this.entry(name);
        if (!entry)
            return [];
        const chain = [];
        let current = entry;
        const stack = [];
        while (current) {
            stack.unshift(current);
            current = current.parent;
        }
        for (const e of stack) {
            if (e.middleware.length > 0) {
                chain.push({ segmentId: e.shortCode, count: e.middleware.length });
            }
        }
        return chain;
    }
    // --- Intercepts ---
    /** Get intercept entries for a named route/layout */
    intercepts(name) {
        const entry = this.entry(name);
        if (!entry)
            return [];
        return entry.intercept.map((i) => ({
            slotName: i.slotName,
            routeName: i.routeName,
            hasWhen: i.when.length > 0,
            whenCount: i.when.length,
            hasLoader: i.loader.length > 0,
            hasMiddleware: i.middleware.length > 0,
        }));
    }
    /** Get raw InterceptEntry objects for a named route/layout */
    interceptEntries(name) {
        return this.entry(name)?.intercept ?? [];
    }
    // --- Loaders ---
    /** Get loader entries for a named route/layout */
    loaders(name) {
        return this.entry(name)?.loader ?? [];
    }
    /** Check if a named route/layout has loaders */
    hasLoaders(name) {
        return this.loaders(name).length > 0;
    }
    // --- Parallel slots ---
    /** Get parallel slot entries (child EntryData with type="parallel") */
    parallelSlots(name) {
        return this.entry(name)?.parallel ?? [];
    }
    /** Get parallel slot names for a named route/layout */
    parallelSlotNames(name) {
        const entry = this.entry(name);
        if (!entry)
            return [];
        // Parallel entry handler is Record<`@${string}`, Handler>
        return entry.parallel.map((p) => {
            if (p.type === "parallel" && typeof p.handler === "object" && p.handler !== null) {
                return Object.keys(p.handler);
            }
            return [];
        }).flat();
    }
    // --- Error/NotFound boundaries ---
    /** Check if a named route/layout has an error boundary */
    hasErrorBoundary(name) {
        return (this.entry(name)?.errorBoundary?.length ?? 0) > 0;
    }
    /** Check if a named route/layout has a not-found boundary */
    hasNotFoundBoundary(name) {
        return (this.entry(name)?.notFoundBoundary?.length ?? 0) > 0;
    }
    // --- Cache ---
    /** Check if a named route/layout has cache config */
    hasCache(name) {
        return this.entry(name)?.cache !== undefined;
    }
    // --- Loading ---
    /** Check if a named route/layout has a loading component */
    hasLoading(name) {
        const entry = this.entry(name);
        if (!entry)
            return false;
        return "loading" in entry && entry.loading !== undefined;
    }
    // --- Debug ---
    /**
     * Debug print the route tree.
     * Useful for exploring the structure in tests.
     */
    debug() {
        const lines = ["Route Tree:"];
        for (const [name, pattern] of this.patterns) {
            const entry = this.entry(name);
            const segPath = this.segmentPath(name);
            const ids = segPath.map((s) => s.id).join(" > ");
            const extras = [];
            if (entry && entry.middleware.length > 0)
                extras.push(`mw:${entry.middleware.length}`);
            if (entry && entry.loader.length > 0)
                extras.push(`ld:${entry.loader.length}`);
            if (entry && entry.intercept.length > 0)
                extras.push(`int:${entry.intercept.length}`);
            if (entry && entry.parallel.length > 0)
                extras.push(`par:${entry.parallel.length}`);
            if (entry?.errorBoundary?.length)
                extras.push("err");
            if (entry?.cache)
                extras.push("cache");
            const suffix = extras.length > 0 ? ` {${extras.join(", ")}}` : "";
            lines.push(`  ${name}: ${pattern} [${entry?.shortCode}] (${ids})${suffix}`);
        }
        return lines.join("\n");
    }
}
// ============================================================================
// Internal helpers
// ============================================================================
function findLazyIncludes(items) {
    const result = [];
    for (const item of items) {
        if (!item)
            continue;
        if (item.type === "include") {
            const includeItem = item;
            if (includeItem.lazy === true && includeItem._lazyContext) {
                result.push({
                    prefix: includeItem.prefix,
                    patterns: includeItem.patterns,
                    context: includeItem._lazyContext,
                });
            }
        }
        if (item.uses && Array.isArray(item.uses)) {
            result.push(...findLazyIncludes(item.uses));
        }
    }
    return result;
}
//# sourceMappingURL=route-tree.js.map