// Mock @skyramp/skyramp so workspaceAuth's import resolves; readWorkspaceConfigRaw
// itself is mocked per-test below.
jest.mock("@skyramp/skyramp", () => ({
    WorkspaceConfigManager: { create: jest.fn() },
}));
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { frameworkFileToUrlPath, findCandidatePagesByFrameworkRoute, findCandidatePagesBySourceRoute, findRootFallbackPage, enumerateCandidateUiPages, pickFrontendBaseUrl, detectsFilesystemRouting, } from "./uiPageEnumerator.js";
import * as workspaceAuth from "./workspaceAuth.js";
// Helper to create a tmp repo dir with optional config files; returns the path.
function makeTmpRepo(configFiles = []) {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-test-repo-"));
    for (const cfg of configFiles) {
        const fullPath = path.join(dir, cfg);
        fs.mkdirSync(path.dirname(fullPath), { recursive: true });
        fs.writeFileSync(fullPath, "");
    }
    return dir;
}
// ---------------------------------------------------------------------------
// frameworkFileToUrlPath
// ---------------------------------------------------------------------------
describe("frameworkFileToUrlPath", () => {
    it("maps Next.js App Router page.tsx to /path", () => {
        expect(frameworkFileToUrlPath("app/orders/page.tsx")).toBe("/orders");
    });
    it("maps Next.js App Router root page.tsx to /", () => {
        expect(frameworkFileToUrlPath("app/page.tsx")).toBe("/");
    });
    it("maps Next.js App Router dynamic [id] segment to :id", () => {
        expect(frameworkFileToUrlPath("app/orders/[id]/page.tsx")).toBe("/orders/:id");
    });
    it("strips Next.js App Router route groups (parentheses)", () => {
        expect(frameworkFileToUrlPath("app/(marketing)/about/page.tsx")).toBe("/about");
    });
    it("maps Next.js Pages Router /pages/foo.tsx to /foo", () => {
        expect(frameworkFileToUrlPath("pages/orders.tsx")).toBe("/orders");
    });
    it("maps Next.js Pages Router /pages/index.tsx to /", () => {
        expect(frameworkFileToUrlPath("pages/index.tsx")).toBe("/");
    });
    it("maps Next.js Pages Router /pages/foo/index.tsx to /foo", () => {
        expect(frameworkFileToUrlPath("pages/orders/index.tsx")).toBe("/orders");
    });
    it("excludes Next.js Pages Router API handlers (/pages/api/*)", () => {
        expect(frameworkFileToUrlPath("pages/api/orders.ts")).toBeNull();
    });
    it("maps Vue / Nuxt /pages/foo.vue to /foo", () => {
        expect(frameworkFileToUrlPath("pages/orders.vue")).toBe("/orders");
    });
    it("maps Vue / Nuxt 2 /pages/_id.vue to :id", () => {
        expect(frameworkFileToUrlPath("pages/_id.vue")).toBe("/:id");
    });
    it("maps SvelteKit routes/+page.svelte to /", () => {
        expect(frameworkFileToUrlPath("src/routes/+page.svelte")).toBe("/");
    });
    it("maps SvelteKit routes/foo/+page.svelte to /foo", () => {
        expect(frameworkFileToUrlPath("src/routes/orders/+page.svelte")).toBe("/orders");
    });
    it("returns null for a generic component file (not a route)", () => {
        expect(frameworkFileToUrlPath("src/components/Button.tsx")).toBeNull();
    });
    it("returns null for a layout file in app/", () => {
        expect(frameworkFileToUrlPath("app/orders/layout.tsx")).toBeNull();
    });
    it("returns null for a styles file under frontend dir", () => {
        expect(frameworkFileToUrlPath("src/styles/global.css")).toBeNull();
    });
});
// ---------------------------------------------------------------------------
// findCandidatePagesByFrameworkRoute
// ---------------------------------------------------------------------------
describe("findCandidatePagesByFrameworkRoute", () => {
    it("returns empty for empty input", () => {
        expect(findCandidatePagesByFrameworkRoute([], "http://localhost:3000")).toEqual([]);
    });
    it("returns empty when no files map to routes", () => {
        expect(findCandidatePagesByFrameworkRoute(["src/components/Button.tsx", "src/utils/format.ts"], "http://localhost:3000")).toEqual([]);
    });
    it("emits a single page with full URL for a Next.js App Router route", () => {
        const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx"], "http://localhost:3000");
        expect(pages).toEqual([
            {
                url: "http://localhost:3000/orders",
                sourcedFrom: ["app/orders/page.tsx"],
                strategy: "framework-route-grep",
            },
        ]);
    });
    it("collapses two files emitting the same URL into one entry with both sources", () => {
        const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx", "app/orders/page.tsx"], "http://localhost:3000");
        expect(pages).toHaveLength(1);
        expect(pages[0].sourcedFrom).toEqual(["app/orders/page.tsx"]);
    });
    it("strips trailing slash from baseUrl before joining", () => {
        const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx"], "http://localhost:3000/");
        expect(pages[0].url).toBe("http://localhost:3000/orders");
    });
    it("ignores files that don't map to routes (mixed input)", () => {
        const pages = findCandidatePagesByFrameworkRoute([
            "src/components/Button.tsx", // not a route
            "app/orders/page.tsx", // route
            "src/styles/main.css", // not a route
        ], "http://localhost:3000");
        expect(pages).toHaveLength(1);
        expect(pages[0].url).toBe("http://localhost:3000/orders");
    });
});
// ---------------------------------------------------------------------------
// findCandidatePagesBySourceRoute (strategy 2)
// ---------------------------------------------------------------------------
describe("findCandidatePagesBySourceRoute", () => {
    function makeRouterRepo() {
        const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-strat2-"));
        fs.mkdirSync(path.join(dir, "src", "pages"), { recursive: true });
        fs.writeFileSync(path.join(dir, "src", "App.tsx"), `
import { Routes, Route } from 'react-router-dom';
import Cart from './pages/Cart';
import Orders from './pages/Orders';

export default function App() {
  return (
    <Routes>
      <Route path="/cart" element={<Cart />} />
      <Route path="/orders" element={<Orders />} />
    </Routes>
  );
}
`);
        fs.writeFileSync(path.join(dir, "src", "pages", "Cart.tsx"), "export default function Cart() { return <div/>; }");
        fs.writeFileSync(path.join(dir, "src", "pages", "Orders.tsx"), "export default function Orders() { return <div/>; }");
        return dir;
    }
    it("emits candidate pages for changed files that match a route's component", () => {
        const repo = makeRouterRepo();
        const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000", repo);
        expect(pages).toHaveLength(1);
        expect(pages[0]).toMatchObject({
            url: "http://localhost:3000/cart",
            strategy: "source-grounded-routes",
        });
        expect(pages[0].sourcedFrom).toEqual(["src/pages/Cart.tsx"]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("emits multiple pages when multiple changed files match routes", () => {
        const repo = makeRouterRepo();
        const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx", "src/pages/Orders.tsx"], "http://localhost:3000", repo);
        expect(pages.map((p) => p.url).sort()).toEqual([
            "http://localhost:3000/cart",
            "http://localhost:3000/orders",
        ]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("returns empty when no changed files match a route's component", () => {
        const repo = makeRouterRepo();
        const pages = findCandidatePagesBySourceRoute(["src/components/Button.tsx"], "http://localhost:3000", repo);
        expect(pages).toEqual([]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("returns empty when the repo has no router files", () => {
        const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-empty-"));
        const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000", repo);
        expect(pages).toEqual([]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("strips trailing slash from baseUrl", () => {
        const repo = makeRouterRepo();
        const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000/", repo);
        expect(pages[0].url).toBe("http://localhost:3000/cart");
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("emits candidate pages when the changed file is the route DECLARATION (router.tsx, not the component)", () => {
        // Regression: previously we only matched route.componentFile. A PR that
        // renames a route in App.tsx but doesn't touch the component itself would
        // surface zero source-grounded candidates. Real example: directus P06
        // renamed /settings/data-model in modules/settings/index.ts; the component
        // was unchanged, so the agent fell through to root-fallback and missed
        // the changed routes entirely.
        const repo = makeRouterRepo();
        const pages = findCandidatePagesBySourceRoute(["src/App.tsx"], "http://localhost:3000", repo);
        expect(pages.map((p) => p.url).sort()).toEqual([
            "http://localhost:3000/cart",
            "http://localhost:3000/orders",
        ]);
        expect(pages[0].sourcedFrom).toEqual(["src/App.tsx"]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
});
// ---------------------------------------------------------------------------
// pickFrontendBaseUrl
// ---------------------------------------------------------------------------
describe("pickFrontendBaseUrl", () => {
    let readSpy;
    beforeEach(() => {
        readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
    });
    afterEach(() => {
        readSpy.mockRestore();
    });
    it("returns null when workspace config is missing", async () => {
        readSpy.mockResolvedValue(null);
        expect(await pickFrontendBaseUrl("/repo")).toBeNull();
    });
    it("returns null when no services have baseUrl", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "backend", language: "python" }],
        });
        expect(await pickFrontendBaseUrl("/repo")).toBeNull();
    });
    it("picks the frontend service by name suffix", async () => {
        readSpy.mockResolvedValue({
            services: [
                { serviceName: "demoshop-backend", api: { baseUrl: "http://localhost:8000" } },
                { serviceName: "demoshop-frontend", api: { baseUrl: "http://localhost:5173" } },
            ],
        });
        expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:5173");
    });
    it("picks the frontend service by framework when name doesn't signal it", async () => {
        readSpy.mockResolvedValue({
            services: [
                { serviceName: "api", language: "python", api: { baseUrl: "http://localhost:8000" } },
                {
                    serviceName: "client",
                    language: "typescript",
                    framework: "next",
                    api: { baseUrl: "http://localhost:3000" },
                },
            ],
        });
        expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
    });
    it("falls back to first service with baseUrl when no frontend signal", async () => {
        readSpy.mockResolvedValue({
            services: [
                { serviceName: "svc-a", language: "go", api: { baseUrl: "http://localhost:8000" } },
                { serviceName: "svc-b", language: "go", api: { baseUrl: "http://localhost:8001" } },
            ],
        });
        expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:8000");
    });
});
// ---------------------------------------------------------------------------
// findRootFallbackPage
// ---------------------------------------------------------------------------
describe("findRootFallbackPage", () => {
    let readSpy;
    beforeEach(() => {
        readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
    });
    afterEach(() => {
        readSpy.mockRestore();
    });
    it("returns null when no frontend baseUrl resolvable", async () => {
        readSpy.mockResolvedValue(null);
        expect(await findRootFallbackPage("/repo")).toBeNull();
    });
    it("returns root-fallback page when frontend baseUrl resolves", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
        });
        const page = await findRootFallbackPage("/repo");
        expect(page).toEqual({
            url: "http://localhost:3000",
            sourcedFrom: [],
            strategy: "root-fallback",
        });
    });
});
// ---------------------------------------------------------------------------
// enumerateCandidateUiPages
// ---------------------------------------------------------------------------
describe("enumerateCandidateUiPages", () => {
    let readSpy;
    beforeEach(() => {
        readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
    });
    afterEach(() => {
        readSpy.mockRestore();
    });
    it("returns empty for empty file list", async () => {
        expect(await enumerateCandidateUiPages("/repo", [])).toEqual([]);
    });
    it("returns empty when workspace has no resolvable baseUrl", async () => {
        readSpy.mockResolvedValue(null);
        expect(await enumerateCandidateUiPages("/repo", ["app/orders/page.tsx"])).toEqual([]);
    });
    it("uses strategy 1 (framework-route-grep) when route files match", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
        });
        const repo = makeTmpRepo(["next.config.js"]);
        const pages = await enumerateCandidateUiPages(repo, [
            "app/orders/page.tsx",
            "src/components/Button.tsx", // ignored — not a route
        ]);
        expect(pages).toHaveLength(1);
        expect(pages[0]).toMatchObject({
            url: "http://localhost:3000/orders",
            strategy: "framework-route-grep",
        });
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("falls back to strategy 3 (root-fallback) when no route files match", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
        });
        const pages = await enumerateCandidateUiPages("/repo", [
            "src/components/Button.tsx", // not a route
            "src/state/store.ts", // not a route
        ]);
        expect(pages).toHaveLength(1);
        expect(pages[0]).toMatchObject({
            url: "http://localhost:3000",
            strategy: "root-fallback",
        });
    });
    it("emits multiple route pages when several match strategy 1", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
        });
        // Need a real repoPath with framework config so detectsFilesystemRouting passes.
        const repo = makeTmpRepo(["next.config.js"]);
        const pages = await enumerateCandidateUiPages(repo, [
            "app/orders/page.tsx",
            "app/products/[id]/page.tsx",
        ]);
        expect(pages).toHaveLength(2);
        const urls = pages.map((p) => p.url).sort();
        expect(urls).toEqual([
            "http://localhost:3000/orders",
            "http://localhost:3000/products/:id",
        ]);
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("uses strategy 2 (source-grounded-routes) when strategy 1 doesn't match but routes are declared in source", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:5173" } }],
        });
        // Repo with no framework config (so strategy 1 won't fire) but with a
        // React Router App.tsx declaring routes.
        const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-ladder-"));
        fs.mkdirSync(path.join(repo, "src", "pages"), { recursive: true });
        fs.writeFileSync(path.join(repo, "src", "App.tsx"), `
import { Routes, Route } from 'react-router-dom';
import Cart from './pages/Cart';
export default function App() { return <Routes><Route path="/cart" element={<Cart />} /></Routes>; }
`);
        fs.writeFileSync(path.join(repo, "src", "pages", "Cart.tsx"), "export default function Cart() { return <div/>; }");
        const pages = await enumerateCandidateUiPages(repo, [
            "src/pages/Cart.tsx",
        ]);
        expect(pages).toHaveLength(1);
        expect(pages[0]).toMatchObject({
            url: "http://localhost:5173/cart",
            strategy: "source-grounded-routes",
        });
        fs.rmSync(repo, { recursive: true, force: true });
    });
    it("falls back to root when repo lacks a framework config (no false positives)", async () => {
        readSpy.mockResolvedValue({
            services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
        });
        // Repo has files matching the Pages Router pattern but no next.config —
        // this is the demoshop case (React with conventional layout, no Next.js).
        const repo = makeTmpRepo([]);
        const pages = await enumerateCandidateUiPages(repo, [
            "frontend/src/pages/Cart.tsx",
        ]);
        expect(pages).toHaveLength(1);
        expect(pages[0]).toMatchObject({
            url: "http://localhost:3000",
            strategy: "root-fallback",
        });
        fs.rmSync(repo, { recursive: true, force: true });
    });
});
// ---------------------------------------------------------------------------
// detectsFilesystemRouting
// ---------------------------------------------------------------------------
describe("detectsFilesystemRouting", () => {
    let repo;
    afterEach(() => {
        if (repo) {
            fs.rmSync(repo, { recursive: true, force: true });
            repo = "";
        }
    });
    it("returns false when no framework config exists", () => {
        repo = makeTmpRepo([]);
        expect(detectsFilesystemRouting(repo)).toBe(false);
    });
    it("detects Next.js via next.config.js at repo root", () => {
        repo = makeTmpRepo(["next.config.js"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("detects Next.js via next.config.ts", () => {
        repo = makeTmpRepo(["next.config.ts"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("detects Nuxt via nuxt.config.ts", () => {
        repo = makeTmpRepo(["nuxt.config.ts"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("detects SvelteKit via svelte.config.js", () => {
        repo = makeTmpRepo(["svelte.config.js"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("detects framework config one level deep (frontend/)", () => {
        repo = makeTmpRepo(["frontend/next.config.js"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("detects framework config in apps/web/", () => {
        repo = makeTmpRepo(["apps/web/next.config.mjs"]);
        expect(detectsFilesystemRouting(repo)).toBe(true);
    });
    it("returns false when only a pages/ directory exists (no config)", () => {
        repo = makeTmpRepo(["pages/Cart.tsx"]);
        expect(detectsFilesystemRouting(repo)).toBe(false);
    });
});
