/**
 * End-to-end coverage of the FastAPI `include_router(prefix=…)` fix.
 *
 * The PR claims to update both layers — ingestion (graph `Route`
 * nodes) and group (HTTP contracts). The group side is exercised by
 * `test/unit/group/http-route-extractor.test.ts`; this file pins the
 * **ingestion** side by running the full pipeline against a realistic
 * fixture and inspecting the resulting `Route` graph nodes.
 *
 * What this test pins:
 *
 * 1. **Shape A** (`from api import users` +
 *    `application.include_router(users.router, prefix='/users')`)
 *    produces `Route` nodes whose `name` is the prefixed full path
 *    (`/users/list`, `/users/create`) — not the bare decorator path.
 *
 * 2. **Shape B with relative import**
 *    (`from .calls import router as calls_router` +
 *    `application.include_router(calls_router, prefix='/calls')`)
 *    works end-to-end. Before the regex fix, the leading-dot module
 *    path was rejected and the prefix was silently dropped.
 *
 * 3. **Same-name modules in different packages** do not bleed
 *    prefixes. Before the long-key fix, both `api/users.py` and
 *    `admin/users.py` shared the basename `users`, so `admin/users`
 *    routes inherited the `/users` prefix that was only meant for
 *    `api/users.py`.
 *
 * 4. **Non-`app` host names** (`application = FastAPI()`) work in the
 *    ingestion regex. The group-layer counterpart is pinned by the
 *    `non-app host` cases in `http-route-extractor.test.ts`.
 *
 * The fixture lives at `test/fixtures/fastapi-prefix-app/` so the
 * pipeline can scan a real on-disk repo (mirroring how `gitnexus
 * analyze` is used in production) and so reviewers can inspect the
 * inputs without reading test source.
 */

import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../types/pipeline.js';

const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'fastapi-prefix-app');

describe('FastAPI include_router(prefix=…) — ingestion pipeline', () => {
  let result: PipelineResult;

  beforeAll(async () => {
    // Force the worker-pool code path on this small fixture (~5
    // files). Without this the pipeline takes the sequential
    // fallback, which historically does NOT run the FastAPI router
    // bindings extractor — the very behaviour we want to pin lives
    // exclusively inside the worker entry point.
    result = await runPipelineFromRepo(FIXTURE, () => {}, {});
  }, 60_000);

  function routeNames(): string[] {
    const out: string[] = [];
    result.graph.forEachNode((n) => {
      if (n.label === 'Route') out.push(String(n.properties.name));
    });
    return out.sort();
  }

  it('joins Shape-A `<mod>.router` prefixes with sub-router decorator paths', () => {
    // `application.include_router(users.router, prefix='/users')` in
    // main.py + `@router.get('/list')` / `@router.post('/create')` in
    // api/users.py → `/users/list`, `/users/create`.
    const names = routeNames();
    expect(names).toContain('/users/list');
    expect(names).toContain('/users/create');
    // The bare decorator paths must NOT survive when a prefix
    // mapping exists — one router yields exactly one Route node per
    // prefix, not the prefixed AND the unprefixed copy.
    expect(names.filter((n) => n === '/list')).toHaveLength(0);
    expect(names.filter((n) => n === '/create')).toHaveLength(0);
  });

  it('joins Shape-B with absolute named import (`from api.calls import router as …`)', () => {
    // main.py mounts `api.calls` under `/calls`; api/calls.py has
    // `@router.get('/list')`. Long-key resolution is required here:
    // `users` (under `/users`) and `calls` are distinct module
    // basenames, but the long key (`api/calls`) is what makes the
    // binding deterministic.
    const names = routeNames();
    expect(names).toContain('/calls/list');
  });

  it('joins Shape-B with relative import (`from .relative import router as …`)', () => {
    // FINDING 2: the worker regex `[A-Za-z_][\w.]*` used to reject
    // module paths starting with `.`, silently dropping every
    // leading-dot relative import. relative.py declares `/info`; the
    // expected joined route is `/rel/info`.
    const names = routeNames();
    expect(names).toContain('/rel/info');
  });

  it('does NOT bleed `/users` prefix onto the same-name `admin/users.py`', () => {
    // FINDING 3: `api/users.py` and `admin/users.py` collide on the
    // short module key `users`. main.py only mounts the `api/users`
    // router under `/users`, so the admin file's `@router.get('/audit')`
    // must surface as the bare `/audit` — never as `/users/audit`.
    const names = routeNames();
    expect(names).toContain('/audit');
    expect(names.filter((n) => n === '/users/audit')).toHaveLength(0);
  });

  it('emits exactly one Route node per (router method, prefix) pair', () => {
    // Defence-in-depth: counts the unique route nodes for the
    // prefixed routes to make sure the duplication path in
    // parse-impl (`for prefix of prefixes`) didn't accidentally
    // double-emit when only a single prefix was registered.
    const names = routeNames();
    const counts = new Map<string, number>();
    for (const n of names) counts.set(n, (counts.get(n) ?? 0) + 1);
    expect(counts.get('/users/list')).toBe(1);
    expect(counts.get('/users/create')).toBe(1);
    expect(counts.get('/calls/list')).toBe(1);
    expect(counts.get('/rel/info')).toBe(1);
    expect(counts.get('/audit')).toBe(1);
  });
});
