import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

/**
 * The doc-lint suites live in two hand-maintained lists in package.json: the `test:docs` script
 * runs them as explicit positionals, and the jest `testPathIgnorePatterns` array keeps them out of
 * the default unit lane. Nothing else knows the lists are a pair, so a suite added to one but not
 * the other drifts silently — excluded from `jest` yet never enumerated by `test:docs`, it runs in
 * no lane at all. And a rename that only reaches one list empties that lane the same way. Only a
 * spec that reads package.json the way both lanes do can see either drift.
 */
describe('Test lane partition', () => {
  const repoRoot = join(__dirname, '..', '..');
  const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')) as {
    scripts: { 'test:docs'?: string };
    jest?: { testPathIgnorePatterns?: string[]; coveragePathIgnorePatterns?: string[] };
  };

  /** Every spec path the `test:docs` script names as a positional, however the flags are ordered. */
  const docLane = (): Set<string> =>
    new Set((pkg.scripts['test:docs'] ?? '').split(/\s+/).filter(token => token.endsWith('.spec.ts')));

  /** The ignore patterns with `/node_modules/` dropped — it excludes dependency code, not a lane. */
  const unitLaneExcludes = (): string[] =>
    (pkg.jest?.testPathIgnorePatterns ?? []).filter(pattern => pattern !== '/node_modules/');

  /** A spec path as the ignore patterns write it: dots escaped, anchored at the end. */
  const asPattern = (path: string): string => `${path.replace(/\./g, '\\.')}$`;

  /** An ignore pattern back as the spec path the script would name. */
  const asPath = (pattern: string): string => pattern.replace(/\\\./g, '.').replace(/\$$/, '');

  it('lists the same suites in the test:docs script and the ignore patterns', () => {
    const docs = docLane();
    const excluded = unitLaneExcludes();

    // Guard the parser: lists that scanned to nothing would make the equality below pass vacuously.
    expect(docs.size).toBeGreaterThan(20);
    expect(excluded.length).toBeGreaterThan(20);

    const onlyInScript = [...docs].filter(path => !excluded.includes(asPattern(path)));
    const onlyInIgnored = excluded.filter(pattern => !docs.has(asPath(pattern)));
    if (onlyInScript.length || onlyInIgnored.length) {
      throw new Error(
        'test:docs and testPathIgnorePatterns name different suites — a suite listed in one but ' +
          'not the other runs in no lane (the unit lane ignores it, the docs lane never invokes ' +
          'it). Add the suite to both lists in package.json. ' +
          `Only in test:docs: ${onlyInScript.join(', ') || '(none)'}. ` +
          `Only in testPathIgnorePatterns: ${onlyInIgnored.join(', ') || '(none)'}.`,
      );
    }
  });

  /**
   * `coveragePathIgnorePatterns` exists to keep the doc-lint suites out of the coverage
   * DENOMINATORS: excluded from the unit lane, they would otherwise be counted as source files with
   * 0% coverage and drag every floor down. So it has to mirror the ignore list, and it has to
   * contain nothing else.
   *
   * The second half is the one that bites. Adding a real source path here removes that code from
   * every ratio silently: the floors keep passing, on a smaller denominator, and coverage that
   * never existed reads as coverage that does. The commit that introduced the array said it was
   * "enforced by the lane-partition spec"; it never was, and this is that enforcement.
   */
  it('keeps the coverage ignore list a mirror of the test ignore list, and nothing more', () => {
    const testIgnores = pkg.jest?.testPathIgnorePatterns ?? [];
    const coverageIgnores = pkg.jest?.coveragePathIgnorePatterns ?? [];

    // Vacuity guard: two empty arrays are trivially equal.
    expect(coverageIgnores.length).toBeGreaterThan(20);

    const testSet = new Set(testIgnores);
    const coverageSet = new Set(coverageIgnores);
    expect({
      onlyInCoverage: [...coverageSet].filter(p => !testSet.has(p)),
      onlyInTest: [...testSet].filter(p => !coverageSet.has(p)),
    }).toEqual({ onlyInCoverage: [], onlyInTest: [] });

    // A pattern that is neither `/node_modules/` nor a single spec file is a source path, and a
    // source path here deletes that code from the coverage ratios.
    const notASpec = coverageIgnores.filter(p => p !== '/node_modules/' && !p.endsWith('\\.spec\\.ts$'));
    expect(notASpec).toEqual([]);
  });

  it('names suites that exist on disk', () => {
    const paths = [...new Set([...docLane(), ...unitLaneExcludes().map(asPath)])];
    expect(paths.length).toBeGreaterThan(20);

    // A renamed spec leaves a stale entry in whichever list was not updated — and an empty lane.
    const missing = paths.filter(path => !existsSync(join(repoRoot, path)));
    expect(missing).toEqual([]);
  });
});
