/**
 * Production-path regression guard for the PHP import-resolution index (#2901).
 *
 * PHP was the last language resolving imports with a full workspace scan per
 * import. Both adapters in `languages/php/import-target.ts` materialized
 * `[...allFilePaths]` twice per import and handed `resolvePhpImportInternal` an
 * `index` of `undefined`, dropping it onto `suffixResolve`'s linear `findIndex`
 * — a pass over every file per path-part × per extension, 98 ms per import at
 * 20k files. They now read the shared `getWorkspaceFileIndex`
 * (`import-resolvers/workspace-file-index.ts`), memoized on the `allFilePaths`
 * Set identity via a WeakMap, through a PHP-specific parity view that keeps the
 * three index-fed fast paths answering exactly what the scans answered (see the
 * `#2901` header in `import-target.ts` — passing the raw shared index straight
 * through MOVES IMPORTS edges, and `test/unit/scope-resolution/
 * php-import-target-parity.test.ts` is the differential that proves this one
 * does not).
 *
 * Resolution reaches that index through `phpScopeResolver.resolveImportTarget`
 * — the orchestrator adapter — not by calling `resolvePhpImportTargetInternal`
 * directly the way the unit parity test does. The adapter must therefore pass
 * the Set THROUGH; a defensive copy (`new Set(allFilePaths)`) would hand a
 * fresh WeakMap key per call and restore the per-import rebuild. Python hit
 * exactly that (PR #1918 review P1), and the parity test cannot see it: it
 * never crosses the adapter.
 *
 * The traversal-count assertions are the perf guard. They are paired with
 * result assertions on purpose: a count of 1 is equally true of an adapter that
 * has stopped resolving anything at all, so counting alone would stay green
 * while every PHP IMPORTS edge disappeared.
 *
 * The no-Composer arm below separately pins a proper suffix hit and a root-file
 * miss. That pair guards the PHP parity view itself; a raw shared-index handoff
 * would make the root file resolve even though the traversal count stayed one.
 */
import { describe, it, expect } from 'vitest';
import { phpScopeResolver } from '../../src/core/ingestion/languages/php/scope-resolver.js';
import type { ComposerConfig } from '../../src/core/ingestion/language-config.js';
import { CountingSet, expectDistinctFileSetsGetOwnIndex } from '../helpers/counting-file-set.js';

const { resolveImportTarget } = phpScopeResolver;

const FROM_FILE = 'app/Main.php';

/** The `composer.json` PSR-4 map `loadPhpComposerConfig` would have produced. */
const COMPOSER: ComposerConfig = { psr4: new Map([['App', 'app']]) };

/**
 * A synthetic PSR-4 app: many service classes, plus the shapes the three
 * index-fed legs answer — `app/Models/User.php` for the class-style whole-path
 * hit, the populated `app/Models/` directory for the function-import fallback,
 * and `lib/Legacy/Helper.php` for the suffix fallback that runs when no PSR-4
 * prefix matches.
 */
function buildWorkspace(fileCount: number): CountingSet {
  const files: string[] = [];
  for (let i = 0; i < fileCount; i++) {
    files.push(`app/Services/Service${String(i).padStart(5, '0')}.php`);
  }
  files.push('app/Models/User.php');
  files.push('app/Models/functions.php');
  files.push('lib/Legacy/Helper.php');
  files.push('index.php');
  files.push(FROM_FILE);
  return new CountingSet(files);
}

describe('PHP import resolution — index reuse across use-statements (#2901)', () => {
  it('builds the workspace index once for many imports over a stable file set', () => {
    const files = buildWorkspace(300);
    const resolved: (string | readonly string[] | null)[] = [];

    for (let i = 0; i < 200; i++) {
      // A PSR-4 class hit, a function import that falls back to the namespace
      // directory, and a third-party namespace that the Composer authority
      // gate rejects before suffix fallback.
      resolved.push(resolveImportTarget('App\\Models\\User', FROM_FILE, files, COMPOSER));
      resolved.push(resolveImportTarget('App\\Models\\getUser', FROM_FILE, files, COMPOSER));
      resolved.push(resolveImportTarget(`Psr\\Log\\Missing${i}`, FROM_FILE, files, COMPOSER));
    }

    expect(files.scans).toBe(1);

    // Paired result assertions — a count of 1 must not be the count of an
    // adapter that resolves nothing.
    expect(resolved[0]).toBe('app/Models/User.php');
    expect(resolved[1]).toBe('app/Models/User.php');
    expect(resolved[2]).toBeNull();
  });

  it('builds the workspace index once with no composer.json at all', () => {
    const files = buildWorkspace(300);
    const resolved: (string | readonly string[] | null)[] = [];

    // `loadResolutionConfig` returns null when the repo has no composer.json,
    // which skips the PSR-4 block entirely and leaves `suffixResolve` — the leg
    // that used to cost a `findIndex` pass per extension — as the only path.
    for (let i = 0; i < 200; i++) {
      resolved.push(resolveImportTarget('Legacy\\Helper', FROM_FILE, files, null));
      resolved.push(resolveImportTarget('index', FROM_FILE, files, null));
      resolved.push(resolveImportTarget(`Psr\\Log\\Missing${i}`, FROM_FILE, files, null));
    }

    expect(files.scans).toBe(1);
    expect(resolved[0]).toBe('lib/Legacy/Helper.php');
    expect(resolved[1]).toBeNull();
    expect(resolved[2]).toBeNull();
  });

  it('a distinct file set gets its own index (no stale cross-run reuse)', () => {
    expectDistinctFileSetsGetOwnIndex({
      resolveImportTarget,
      buildWorkspace: () => buildWorkspace(20),
      targetRaw: 'App\\Models\\User',
      fromFile: FROM_FILE,
      resolutionConfig: COMPOSER,
      expected: 'app/Models/User.php',
      expectedScans: 1,
    });
  });

  it('still resolves real use-statements correctly (the perf test is not vacuous)', () => {
    const files = buildWorkspace(5);

    // PSR-4 class-style: `App\Models\User` → `app/Models/User.php`.
    expect(resolveImportTarget('App\\Models\\User', FROM_FILE, files, COMPOSER)).toBe(
      'app/Models/User.php',
    );
    expect(resolveImportTarget('App\\Services\\Service00000', FROM_FILE, files, COMPOSER)).toBe(
      'app/Services/Service00000.php',
    );

    // Composer's non-empty PSR-4 map is authoritative: an unmatched namespace
    // belongs outside the repository and cannot fall through to a local suffix.
    expect(resolveImportTarget('Legacy\\Helper', FROM_FILE, files, COMPOSER)).toBeNull();

    // A root-level file is NOT reachable as a proper suffix — the pre-#2901
    // behaviour the parity view preserves, and the single most likely thing a
    // raw `getWorkspaceFileIndex().index` hand-off would have changed.
    expect(resolveImportTarget('index', FROM_FILE, files, COMPOSER)).toBeNull();

    // Third-party namespaces have no file in the repo.
    expect(resolveImportTarget('Psr\\Log\\LoggerInterface', FROM_FILE, files, COMPOSER)).toBeNull();
  });
});
