// @ts-ignore
import { nextjsFileToApiPath, parseRouteLine, parseFileEndpoints, extractResourceFromPath, classifyEndpointsByChangedFiles } from "./routeParsers.js";
describe("nextjsFileToApiPath", () => {
    it("converts pages/api route to API path", () => {
        expect(nextjsFileToApiPath("pages/api/users/index.ts")).toBe("/api/users");
    });
    it("converts pages/api nested dynamic route", () => {
        expect(nextjsFileToApiPath("pages/api/users/[id].ts")).toBe("/api/users/{id}");
    });
    it("converts app/api route (App Router)", () => {
        expect(nextjsFileToApiPath("app/api/products/route.ts")).toBe("/api/products");
    });
    it("converts app/api nested dynamic route", () => {
        expect(nextjsFileToApiPath("app/api/products/[id]/route.ts")).toBe("/api/products/{id}");
    });
    it("returns null for non-API paths", () => {
        expect(nextjsFileToApiPath("src/components/Button.tsx")).toBeNull();
    });
});
describe("parseRouteLine", () => {
    it("parses FastAPI decorator", () => {
        const result = parseRouteLine('@app.get("/users")', "main.py");
        expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "main.py" });
    });
    it("parses FastAPI with router prefix", () => {
        const result = parseRouteLine('@router.post("/items")', "routes.py");
        expect(result).toEqual({ method: "POST", path: "/items", sourceFile: "routes.py" });
    });
    it("parses Flask decorator", () => {
        const result = parseRouteLine('@app.route("/health", methods=["GET"])', "app.py");
        expect(result).toEqual({ method: "GET", path: "/health", sourceFile: "app.py" });
    });
    it("parses Express route", () => {
        const result = parseRouteLine('router.get("/api/products", handler)', "routes.ts");
        expect(result).toEqual({ method: "GET", path: "/api/products", sourceFile: "routes.ts" });
    });
    it("parses NestJS decorator", () => {
        const result = parseRouteLine('@Get("users/:id")', "users.controller.ts");
        expect(result).toEqual({ method: "GET", path: "users/:id", sourceFile: "users.controller.ts" });
    });
    it("parses Spring annotation", () => {
        const result = parseRouteLine('@GetMapping("/api/v1/orders")', "OrderController.java");
        expect(result).toEqual({ method: "GET", path: "/api/v1/orders", sourceFile: "OrderController.java" });
    });
    it("parses Next.js App Router export", () => {
        const result = parseRouteLine("export async function GET(request: Request) {", "app/api/items/route.ts");
        expect(result).not.toBeNull();
        expect(result.method).toBe("GET");
    });
    it("parses Next.js guard clause (req.method !== 'POST')", () => {
        const result = parseRouteLine('  if (req.method !== "POST") {', "pages/api/v1/collections/[id]/transfer.ts");
        expect(result).not.toBeNull();
        expect(result.method).toBe("POST");
        expect(result.path).toBe("/api/v1/collections/{id}/transfer");
    });
    it("parses Next.js guard clause with != operator", () => {
        const result = parseRouteLine("  if (req.method != 'DELETE') {", "pages/api/v1/items/[id].ts");
        expect(result).not.toBeNull();
        expect(result.method).toBe("DELETE");
        expect(result.path).toBe("/api/v1/items/{id}");
    });
    it("parses Gin route", () => {
        const result = parseRouteLine('r.GET("/users", getUsers)', "main.go");
        expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "main.go" });
    });
    it("parses Gin group route", () => {
        const result = parseRouteLine('api.POST("/products", createProduct)', "router.go");
        expect(result).toEqual({ method: "POST", path: "/products", sourceFile: "router.go" });
    });
    it("parses Rails route", () => {
        const result = parseRouteLine('get "/users", to: "users#index"', "routes.rb");
        expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "routes.rb" });
    });
    it("parses Phoenix route in .ex file", () => {
        const result = parseRouteLine('get "/users", UserController, :index', "lib/my_app_web/router.ex");
        expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "lib/my_app_web/router.ex" });
    });
    it("parses Phoenix route in .exs file", () => {
        const result = parseRouteLine('post "/users", UserController, :create', "lib/my_app_web/router.exs");
        expect(result).toEqual({ method: "POST", path: "/users", sourceFile: "lib/my_app_web/router.exs" });
    });
    it("parses Laravel route", () => {
        const result = parseRouteLine("Route::get('/api/users', [UserController::class, 'index'])", "api.php");
        expect(result).toEqual({ method: "GET", path: "/api/users", sourceFile: "api.php" });
    });
    it("parses Actix attribute", () => {
        const result = parseRouteLine('#[get("/health")]', "main.rs");
        expect(result).toEqual({ method: "GET", path: "/health", sourceFile: "main.rs" });
    });
    it("parses ASP.NET HttpGet", () => {
        const result = parseRouteLine('[HttpGet("users/{id}")]', "UsersController.cs");
        expect(result).toEqual({ method: "GET", path: "users/{id}", sourceFile: "UsersController.cs" });
    });
    it("parses ASP.NET HttpPost without path", () => {
        const result = parseRouteLine("[HttpPost]", "OrdersController.cs");
        expect(result).toEqual({ method: "POST", path: "/", sourceFile: "OrdersController.cs" });
    });
    it("parses Hapi route", () => {
        const result = parseRouteLine("{ method: 'GET', path: '/api/users' }", "server.js");
        expect(result).toEqual({ method: "GET", path: "/api/users", sourceFile: "server.js" });
    });
    it("returns null for non-route lines", () => {
        expect(parseRouteLine("const x = 42;", "app.ts")).toBeNull();
        expect(parseRouteLine("// comment", "main.py")).toBeNull();
        expect(parseRouteLine("import express from 'express';", "index.ts")).toBeNull();
    });
    it("ignores Express-style calls in component files (.tsx, .jsx, .vue, .svelte)", () => {
        expect(parseRouteLine('router.put("/api/orders", handler)', "components/OrderForm.tsx")).toBeNull();
        expect(parseRouteLine('app.get("/products", handler)', "pages/Products.jsx")).toBeNull();
        expect(parseRouteLine('router.post("/items", handler)', "views/ItemForm.vue")).toBeNull();
        expect(parseRouteLine('app.delete("/cart", handler)', "routes/Cart.svelte")).toBeNull();
    });
    it("ignores NestJS decorators in component files", () => {
        expect(parseRouteLine('@Get("/users")', "components/UserList.tsx")).toBeNull();
        expect(parseRouteLine('@Post("/orders")', "forms/OrderForm.jsx")).toBeNull();
    });
    it("ignores Hapi-style objects in component files", () => {
        expect(parseRouteLine("{ method: 'PUT', path: '/api/orders' }", "components/OrderForm.tsx")).toBeNull();
    });
    it("still parses Express routes from .ts backend files", () => {
        const result = parseRouteLine('router.put("/api/orders", handler)', "routes/orders.ts");
        expect(result).toEqual({ method: "PUT", path: "/api/orders", sourceFile: "routes/orders.ts" });
    });
    it("@RequestMapping uses MULTI when no method attr", () => {
        const result = parseRouteLine('@RequestMapping("/api/v1/items")', "ItemController.java");
        expect(result).toEqual({ method: "MULTI", path: "/api/v1/items", sourceFile: "ItemController.java" });
    });
    it("@RequestMapping respects explicit RequestMethod", () => {
        const result = parseRouteLine('@RequestMapping(value="/orders", method=RequestMethod.POST)', "OrderController.java");
        expect(result).toEqual({ method: "POST", path: "/orders", sourceFile: "OrderController.java" });
    });
    it("@RequestMapping uses MULTI when multiple methods specified", () => {
        const result = parseRouteLine('@RequestMapping(value="/bulk", method={RequestMethod.GET, RequestMethod.POST})', "ItemController.java");
        expect(result).toEqual({ method: "MULTI", path: "/bulk", sourceFile: "ItemController.java" });
    });
});
describe("parseFileEndpoints", () => {
    it("Spring: combines @RestController + @RequestMapping class prefix with @GetMapping", () => {
        const content = `
      @RestController
      @RequestMapping("/api/v1/users")
      public class UserController {
        @GetMapping("/{id}")
        public User getUser() {}
        @PostMapping("/")
        public User createUser() {}
      }
    `;
        const eps = parseFileEndpoints(content, "UserController.java");
        expect(eps).toContainEqual({ method: "GET", path: "/api/v1/users/{id}", sourceFile: "UserController.java" });
        expect(eps).toContainEqual({ method: "POST", path: "/api/v1/users", sourceFile: "UserController.java" });
    });
    it("Spring: class without prefix emits method paths as-is", () => {
        const content = `
      public class ProductController {
        @GetMapping("/products")
        public List<Product> list() {}
      }
    `;
        const eps = parseFileEndpoints(content, "ProductController.java");
        expect(eps).toContainEqual({ method: "GET", path: "/products", sourceFile: "ProductController.java" });
    });
    it("Spring: second class in file does not inherit first class prefix", () => {
        const content = `
      @RequestMapping("/api/orders")
      public class OrderController {
        @GetMapping("/{id}")
        public Order get() {}
      }
      public class HealthController {
        @GetMapping("/health")
        public String health() {}
      }
    `;
        const eps = parseFileEndpoints(content, "Controllers.java");
        expect(eps).toContainEqual({ method: "GET", path: "/api/orders/{id}", sourceFile: "Controllers.java" });
        expect(eps).toContainEqual({ method: "GET", path: "/health", sourceFile: "Controllers.java" });
        expect(eps).not.toContainEqual({ method: "GET", path: "/api/orders/health", sourceFile: "Controllers.java" });
    });
    it("NestJS: combines @Controller prefix with @Get path", () => {
        const content = `
      @Controller('users')
      export class UsersController {
        @Get(':id')
        findOne() {}
      }
    `;
        const eps = parseFileEndpoints(content, "users.controller.ts");
        expect(eps).toContainEqual({ method: "GET", path: "users/:id", sourceFile: "users.controller.ts" });
    });
    it("NestJS: @Post() with no path uses controller root", () => {
        const content = `
      @Controller('users')
      export class UsersController {
        @Post()
        create() {}
        @Get()
        findAll() {}
      }
    `;
        const eps = parseFileEndpoints(content, "users.controller.ts");
        expect(eps).toContainEqual({ method: "POST", path: "users", sourceFile: "users.controller.ts" });
        expect(eps).toContainEqual({ method: "GET", path: "users", sourceFile: "users.controller.ts" });
    });
    it("ASP.NET: combines [Route] prefix with [HttpGet] path", () => {
        const content = `
      [ApiController]
      [Route("api/v1/orders")]
      public class OrdersController : ControllerBase {
        [HttpGet("{id}")]
        public IActionResult Get(int id) {}
        [HttpPost]
        public IActionResult Create() {}
      }
    `;
        const eps = parseFileEndpoints(content, "OrdersController.cs");
        expect(eps).toContainEqual({ method: "GET", path: "api/v1/orders/{id}", sourceFile: "OrdersController.cs" });
        expect(eps).toContainEqual({ method: "POST", path: "api/v1/orders", sourceFile: "OrdersController.cs" });
    });
    it("non-class-based frameworks are unaffected (Express, Flask, Gin)", () => {
        const content = `
      router.get("/api/products", handler)
      @app.post("/items")
      r.DELETE("/users/:id", deleteUser)
    `;
        const eps = parseFileEndpoints(content, "routes.ts");
        expect(eps).toContainEqual({ method: "GET", path: "/api/products", sourceFile: "routes.ts" });
    });
    it("FastAPI multi-line APIRouter prefix is combined with method decorators", () => {
        const content = `
router = APIRouter(
    prefix="/orders",
    tags=["orders"],
)

@router.post("")
async def create_order(): pass

@router.get("/{order_id}")
async def get_order(): pass

@router.delete("/{order_id}")
async def delete_order(): pass
    `;
        const eps = parseFileEndpoints(content, "routers/order.py");
        expect(eps).toContainEqual({ method: "POST", path: "/orders", sourceFile: "routers/order.py" });
        expect(eps).toContainEqual({ method: "GET", path: "/orders/{order_id}", sourceFile: "routers/order.py" });
        expect(eps).toContainEqual({ method: "DELETE", path: "/orders/{order_id}", sourceFile: "routers/order.py" });
    });
    it("FastAPI single-line APIRouter prefix is combined with method decorators", () => {
        const content = `
router = APIRouter(prefix="/products", tags=["products"])

@router.get("/{product_id}")
async def get_product(): pass
    `;
        const eps = parseFileEndpoints(content, "routers/product.py");
        expect(eps).toContainEqual({ method: "GET", path: "/products/{product_id}", sourceFile: "routers/product.py" });
    });
    it("FastAPI multi-line method decorators capture path arguments", () => {
        const content = `
router = APIRouter(
    prefix="/flows",
    tags=["flows"],
)

@router.delete(
    "/{id:uuid}",
    status_code=204,
)
async def delete_flow(): pass

@router.post(
    "/bulk_delete",
    status_code=204,
)
async def bulk_delete_flows(): pass
    `;
        const eps = parseFileEndpoints(content, "src/prefect/server/api/flows.py");
        expect(eps).toContainEqual({ method: "DELETE", path: "/flows/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" });
        expect(eps).toContainEqual({ method: "POST", path: "/flows/bulk_delete", sourceFile: "src/prefect/server/api/flows.py" });
    });
    // ── Multi-line Express route patterns ──────────────────────────────────────
    it("Express multi-line: router.post( with path on next line", () => {
        const content = `
router.post(
\t'/:collection',
\tasyncHandler(async (req, res, next) => {})
);
`;
        const eps = parseFileEndpoints(content, "controllers/collections.ts");
        expect(eps).toContainEqual({ method: "POST", path: "/:collection", sourceFile: "controllers/collections.ts" });
    });
    it("Express multi-line: app.get( with path on next line", () => {
        const content = `
app.get(
\t'/health',
\t(req, res) => res.json({ status: 'ok' })
);
`;
        const eps = parseFileEndpoints(content, "server.ts");
        expect(eps).toContainEqual({ method: "GET", path: "/health", sourceFile: "server.ts" });
    });
    it("Express multi-line: does not fire for single-line routes", () => {
        const content = `router.get('/api/products', handler)`;
        const eps = parseFileEndpoints(content, "routes.ts");
        expect(eps).toHaveLength(1);
        expect(eps[0]).toEqual({ method: "GET", path: "/api/products", sourceFile: "routes.ts" });
    });
    it("Express multi-line: handles multiple consecutive multi-line routes", () => {
        const content = `
router.post(
\t'/',
\tasyncHandler(async (req, res) => {})
);

router.get(
\t'/:pk',
\tasyncHandler(async (req, res) => {})
);

router.delete(
\t'/:pk',
\tasyncHandler(async (req, res) => {})
);
`;
        const eps = parseFileEndpoints(content, "controllers/items.ts");
        expect(eps).toHaveLength(3);
        expect(eps).toContainEqual({ method: "POST", path: "/", sourceFile: "controllers/items.ts" });
        expect(eps).toContainEqual({ method: "GET", path: "/:pk", sourceFile: "controllers/items.ts" });
        expect(eps).toContainEqual({ method: "DELETE", path: "/:pk", sourceFile: "controllers/items.ts" });
    });
    it("Express multi-line: ignores component files (.tsx)", () => {
        const content = `
router.post(
\t'/:collection',
\thandler
);
`;
        const eps = parseFileEndpoints(content, "components/Form.tsx");
        expect(eps).toHaveLength(0);
    });
    it("Express multi-line: abandons pending state on closing paren without path", () => {
        const content = `
router.post(
);
router.get('/fallback', handler);
`;
        const eps = parseFileEndpoints(content, "routes.ts");
        expect(eps).toHaveLength(1);
        expect(eps[0]).toEqual({ method: "GET", path: "/fallback", sourceFile: "routes.ts" });
    });
    it("Express multi-line: mixes single-line and multi-line routes", () => {
        const content = `
router.get('/', validateBatch('read'), readHandler, respond);

router.get(
\t'/:pk',
\tasyncHandler(async (req, res, next) => {})
);

router.patch(
\t'/:pk',
\tvalidateBatch('update'),
\tasyncHandler(async (req, res, next) => {})
);
`;
        const eps = parseFileEndpoints(content, "controllers/access.ts");
        expect(eps).toHaveLength(3);
        expect(eps).toContainEqual({ method: "GET", path: "/", sourceFile: "controllers/access.ts" });
        expect(eps).toContainEqual({ method: "GET", path: "/:pk", sourceFile: "controllers/access.ts" });
        expect(eps).toContainEqual({ method: "PATCH", path: "/:pk", sourceFile: "controllers/access.ts" });
    });
});
describe("extractResourceFromPath", () => {
    it("extracts resource from simple path", () => {
        expect(extractResourceFromPath("/orders")).toBe("orders");
    });
    it("strips api and version prefixes", () => {
        expect(extractResourceFromPath("/api/v1/orders")).toBe("orders");
        expect(extractResourceFromPath("/api/v2/products")).toBe("products");
        expect(extractResourceFromPath("/api/v3/items")).toBe("items");
    });
    it("strips path parameters", () => {
        expect(extractResourceFromPath("/api/v1/orders/{order_id}")).toBe("orders");
    });
    it("handles nested resources", () => {
        expect(extractResourceFromPath("/api/v1/orders/{order_id}/items")).toBe("orders_items");
    });
    it("handles public prefix", () => {
        expect(extractResourceFromPath("/public/products")).toBe("products");
    });
    it("returns unknown for empty path", () => {
        expect(extractResourceFromPath("")).toBe("unknown");
    });
    it("returns unknown for path with only skippable segments", () => {
        expect(extractResourceFromPath("/api/v1/{id}")).toBe("unknown");
    });
    it("handles path without leading slash", () => {
        expect(extractResourceFromPath("api/v1/orders")).toBe("orders");
    });
});
describe("classifyEndpointsByChangedFiles", () => {
    function makeDiffDataForClassify(opts) {
        return {
            currentBranch: "feature/test",
            baseBranch: "origin/main",
            changedFiles: opts.changedFiles,
            diffContent: "",
            diffStat: "",
            newFiles: opts.newFiles ?? [],
            deletedFiles: opts.deletedFiles ?? [],
        };
    }
    const scannedEndpoints = [
        { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH"], sourceFile: "src/routes/orders.py" },
        { path: "/api/v1/products/{product_id}", methods: ["GET", "POST"], sourceFile: "src/routes/products.py" },
        { path: "/api/v1/users", methods: ["GET"], sourceFile: "src/routes/users.py" },
    ];
    it("classifies modified endpoints from changed files", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.changedEndpoints).toHaveLength(1);
        expect(result.changedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
        expect(result.newEndpoints).toHaveLength(0);
        expect(result.removedEndpoints).toHaveLength(0);
    });
    it("classifies new endpoints from newly-created files", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/products.py"], newFiles: ["src/routes/products.py"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.newEndpoints).toHaveLength(1);
        expect(result.newEndpoints[0].path).toBe("/api/v1/products/{product_id}");
        expect(result.changedEndpoints).toHaveLength(0);
    });
    it("puts files without endpoints into unmatchedFiles", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["src/models/order.py", "src/routes/orders.py"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.unmatchedFiles).toEqual(["src/models/order.py"]);
        expect(result.changedEndpoints).toHaveLength(1);
    });
    it("skips deleted files from changed classification", () => {
        const deletedEndpoints = [{ path: "/api/v1/legacy", methods: ["GET"], sourceFile: "src/routes/legacy.py" }];
        const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/legacy.py", "src/routes/orders.py"], deletedFiles: ["src/routes/legacy.py"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deletedEndpoints);
        expect(result.removedEndpoints).toHaveLength(1);
        expect(result.removedEndpoints[0].path).toBe("/api/v1/legacy");
        expect(result.changedEndpoints).toHaveLength(1);
        expect(result.changedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
    });
    it("handles multiple endpoints in the same file", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.changedEndpoints).toHaveLength(1);
        expect(result.changedEndpoints[0].methods).toEqual(["GET", "PATCH"]);
    });
    it("returns empty results when no changed files match endpoints", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["README.md", "package.json"] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.changedEndpoints).toHaveLength(0);
        expect(result.newEndpoints).toHaveLength(0);
        expect(result.removedEndpoints).toHaveLength(0);
        expect(result.unmatchedFiles).toEqual(["README.md", "package.json"]);
    });
    it("computes affectedServices from changedFiles", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["services/orders/routes.py", "services/users/models.py"] });
        const result = classifyEndpointsByChangedFiles(diff, []);
        expect(result.affectedServices).toContain("orders");
        expect(result.affectedServices).toContain("users");
    });
    it("propagates branch metadata from diffData", () => {
        const diff = makeDiffDataForClassify({ changedFiles: [] });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.currentBranch).toBe("feature/test");
        expect(result.baseBranch).toBe("origin/main");
        expect(result.changedFiles).toEqual([]);
    });
    it("handles empty scannedEndpoints (all files unmatched)", () => {
        const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
        const result = classifyEndpointsByChangedFiles(diff, []);
        expect(result.changedEndpoints).toHaveLength(0);
        expect(result.newEndpoints).toHaveLength(0);
        expect(result.removedEndpoints).toHaveLength(0);
        expect(result.unmatchedFiles).toEqual(["src/routes/orders.py"]);
    });
    it("does not double-count a file that is both new and has endpoints", () => {
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/products.py", "src/routes/orders.py"],
            newFiles: ["src/routes/products.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        // products.py is new → newEndpoints; orders.py is changed → changedEndpoints
        expect(result.newEndpoints).toHaveLength(1);
        expect(result.newEndpoints[0].sourceFile).toBe("src/routes/products.py");
        expect(result.changedEndpoints).toHaveLength(1);
        expect(result.changedEndpoints[0].sourceFile).toBe("src/routes/orders.py");
    });
    it("includes deleted-file endpoints in removedEndpoints when not in current catalog", () => {
        const deleted = [
            { path: "/api/v1/removed", methods: ["GET", "POST"], sourceFile: "src/routes/removed.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/removed.py"],
            deletedFiles: ["src/routes/removed.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
        expect(result.removedEndpoints).toHaveLength(1);
        expect(result.removedEndpoints[0].path).toBe("/api/v1/removed");
        expect(result.removedEndpoints[0].methods).toEqual(["GET", "POST"]);
        // The deleted file should NOT appear in changedEndpoints or unmatchedFiles
        expect(result.changedEndpoints.every(ep => ep.sourceFile !== "src/routes/removed.py")).toBe(true);
        expect(result.unmatchedFiles).not.toContain("src/routes/removed.py");
    });
    it("filters moved endpoints from removedEndpoints when they exist in current catalog", () => {
        // Endpoint was in a deleted file but the same METHOD+path exists in scanned catalog
        // (moved to another file) — should NOT appear in removedEndpoints
        const deleted = [
            { path: "/api/v1/orders/{order_id}", methods: ["GET", "DELETE"], sourceFile: "src/routes/old-orders.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/old-orders.py"],
            deletedFiles: ["src/routes/old-orders.py"],
        });
        // scannedEndpoints has GET /api/v1/orders/{order_id} (from orders.py) → moved, filter out
        // DELETE /api/v1/orders/{order_id} is NOT in scannedEndpoints → truly removed, keep
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
        expect(result.removedEndpoints).toHaveLength(1);
        expect(result.removedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
        expect(result.removedEndpoints[0].methods).toEqual(["DELETE"]);
    });
    it("filters out entirely moved endpoints (all methods exist in catalog)", () => {
        const deleted = [
            { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH"], sourceFile: "src/routes/old-orders.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/old-orders.py"],
            deletedFiles: ["src/routes/old-orders.py"],
        });
        // Both GET and PATCH for /api/v1/orders/{order_id} exist in scannedEndpoints → fully filtered
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
        expect(result.removedEndpoints).toHaveLength(0);
    });
    it("treats MULTI sentinel in current catalog as wildcard when filtering moved endpoints", () => {
        // Deleted file had explicit GET+POST methods, but current catalog has MULTI for the same path
        // (e.g. moved to a Next.js catch-all handler) — should be fully filtered out
        const catalogWithMulti = [
            ...scannedEndpoints,
            { path: "/api/v1/legacy", methods: ["MULTI"], sourceFile: "pages/api/legacy.ts" },
        ];
        const deleted = [
            { path: "/api/v1/legacy", methods: ["GET", "POST"], sourceFile: "src/routes/legacy.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/legacy.py"],
            deletedFiles: ["src/routes/legacy.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, catalogWithMulti, deleted);
        expect(result.removedEndpoints).toHaveLength(0);
    });
    it("treats MULTI sentinel in deleted endpoints as wildcard when path exists in catalog", () => {
        // Deleted file was a Next.js catch-all (MULTI), but path now exists with concrete methods
        // in the current catalog — should be filtered out as moved, not flagged as removed
        const deleted = [
            { path: "/api/v1/orders/{order_id}", methods: ["MULTI"], sourceFile: "pages/api/orders/[order_id].ts" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["pages/api/orders/[order_id].ts"],
            deletedFiles: ["pages/api/orders/[order_id].ts"],
        });
        // scannedEndpoints already has GET+PATCH for /api/v1/orders/{order_id}
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
        expect(result.removedEndpoints).toHaveLength(0);
    });
    it("matches POSIX diff paths against Windows-style scanned source files", () => {
        const windowsScanned = [
            { path: "/api/v1/orders/{order_id}", methods: ["GET"], sourceFile: "src\\routes\\orders.py" },
            { path: "/api/v1/products/{product_id}", methods: ["POST"], sourceFile: "src\\routes\\products.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/orders.py", "src/routes/products.py", "src/routes/legacy.py"],
            newFiles: ["src/routes/products.py"],
            deletedFiles: ["src/routes/legacy.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, windowsScanned, [
            { path: "/api/v1/legacy", methods: ["DELETE"], sourceFile: "src\\routes\\legacy.py" },
        ]);
        expect(result.changedEndpoints).toEqual([windowsScanned[0]]);
        expect(result.newEndpoints).toEqual([windowsScanned[1]]);
        expect(result.removedEndpoints).toEqual([
            { path: "/api/v1/legacy", methods: ["DELETE"], sourceFile: "src\\routes\\legacy.py" },
        ]);
        expect(result.unmatchedFiles).toEqual([]);
        expect(result.changedFiles).toEqual(diff.changedFiles);
    });
    it("uses Windows path separators when comparing moved endpoint resources", () => {
        const current = [
            { path: "/api/v1/orders/{order_id}", methods: ["GET"], sourceFile: "src/routes/orders.py" },
        ];
        const deleted = [
            { path: "/api/v1/orders/{order_id}", methods: ["GET", "DELETE"], sourceFile: "src\\routes\\old-orders.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src\\routes\\old-orders.py"],
            deletedFiles: ["src\\routes\\old-orders.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, current, deleted);
        expect(result.removedEndpoints).toHaveLength(1);
        expect(result.removedEndpoints[0].methods).toEqual(["DELETE"]);
    });
    it("treats generic same-name route files in the same resource folder as moved", () => {
        const current = [
            { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/server.py" },
        ];
        const deleted = [
            { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/old-server.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/api/flows/old-server.py"],
            deletedFiles: ["src/api/flows/old-server.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, current, deleted);
        expect(result.removedEndpoints).toHaveLength(0);
    });
    it("does not treat generic same-name route files in different resource folders as moved", () => {
        const current = [
            { path: "/{id}", methods: ["GET"], sourceFile: "src/api/deployments/server.py" },
        ];
        const deleted = [
            { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/server.py" },
        ];
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/api/flows/server.py"],
            deletedFiles: ["src/api/flows/server.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, current, deleted);
        expect(result.removedEndpoints).toHaveLength(1);
        expect(result.removedEndpoints[0].methods).toEqual(["GET", "DELETE"]);
    });
    it("defaults removedEndpoints to empty when deletedFileEndpoints is omitted", () => {
        const diff = makeDiffDataForClassify({
            changedFiles: ["src/routes/legacy.py"],
            deletedFiles: ["src/routes/legacy.py"],
        });
        const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
        expect(result.removedEndpoints).toEqual([]);
    });
});
