import { describe, expect, it } from 'vitest';
import {
  cascadeExcludeFeatures,
  extractRename,
  preSanitizeSpec,
  specThinFeatures,
  validateSpec,
} from '../src/core/pipeline';
import type { FeatureNode, FeatureSpec } from '../src/core/types';

const feat = (id: string, over: Partial<FeatureNode> = {}): FeatureNode => ({
  id,
  name: id,
  description: 'd',
  parentId: null,
  deps: [],
  files: [`src/${id.toLowerCase()}.ts`],
  testFile: `tests/${id.toLowerCase()}.test.ts`,
  edgeCases: ['e1'],
  exports: [],
  envVars: [],
  stubs: [{ path: `src/${id.toLowerCase()}.ts`, content: 'export {};\n' }],
  ...over,
});

const spec = (features: FeatureNode[]): FeatureSpec => ({ title: 't', prompt: 'p', features });

describe('deterministic dependency resolution (spec gate never dies on id formats)', () => {
  it('rewrites bare-token deps to the full feature id', () => {
    const s = spec([
      feat('F2_application_shell'),
      feat('F10_article_render'),
      feat('F40_a11y', { deps: ['F2', 'F10'] }),
    ]);
    preSanitizeSpec(s);
    expect(s.features[2].deps).toEqual(['F2_application_shell', 'F10_article_render']);
    expect(validateSpec(s).filter(p => p.includes('unknown'))).toEqual([]);
  });

  it('drops unknown, self, and duplicate deps instead of blocking the gate', () => {
    const s = spec([feat('F1', { deps: ['F1', 'F99', 'F1'] }), feat('F2', { deps: ['F1', 'F1'] })]);
    preSanitizeSpec(s);
    expect(s.features[0].deps).toEqual([]);
    expect(s.features[1].deps).toEqual(['F1']);
  });

  it('ambiguous leading tokens are NOT guessed', () => {
    const s = spec([feat('F2_shell'), feat('F2_dup'), feat('F3', { deps: ['F2'] })]);
    preSanitizeSpec(s);
    expect(s.features[2].deps).toEqual([]); // ambiguous → dropped, surfaced by graph absence
  });
});

describe('thin edge cases: a warning, never a gate error or cascade seed', () => {
  it('validateSpec does not flag empty edgeCases', () => {
    const s = spec([feat('F1', { edgeCases: [] })]);
    preSanitizeSpec(s);
    expect(validateSpec(s).some(p => p.includes('edge cases'))).toBe(false);
    expect(specThinFeatures(s)).toEqual(['F1']);
  });
});

describe('cascade provenance: dependents are collateral, not defects', () => {
  it('cascadeExcludeFeatures pulls dependents; seeds stay identifiable', () => {
    const features = [
      feat('F2_shell'),
      feat('F7_home', { deps: ['F2_shell'] }),
      feat('F8_search', { deps: ['F7_home'] }),
      feat('F30_talk'),
    ];
    const seeds = new Set(['F2_shell']);
    const drop = cascadeExcludeFeatures(seeds, features);
    expect([...drop].sort()).toEqual(['F2_shell', 'F7_home', 'F8_search']);
    const collateral = [...drop].filter(id => !seeds.has(id));
    expect(collateral.sort()).toEqual(['F7_home', 'F8_search']);
    expect(drop.has('F30_talk')).toBe(false);
  });
});

describe("tsc's own suggestion is a zero-token fix (TS2551/2552/2820)", () => {
  it('extracts the rename from a TS2552 message', () => {
    expect(extractRename("Cannot find name 'RouteMap'. Did you mean 'routeMap'?"))
      .toEqual({ wrong: 'RouteMap', right: 'routeMap' });
  });
  it('extracts from a TS2551 property message', () => {
    expect(extractRename("Property 'craete' does not exist on type 'Store'. Did you mean 'create'?"))
      .toEqual({ wrong: 'craete', right: 'create' });
  });
  it('identical or suggestion-free messages yield null', () => {
    expect(extractRename("Cannot find name 'X'. Did you mean 'X'?")).toBeNull();
    expect(extractRename("Cannot find name 'X'.")).toBeNull();
  });
});
