/**
 * Unit tests for `extractParsedFile` — the parse-worker → ScopeExtractor
 * bridge (RFC #909 Ring 2 PKG #920).
 *
 * The goal is to pin three invariants:
 *
 *   1. When a provider does NOT implement `emitScopeCaptures`, the helper
 *      returns `undefined` silently. This is the state of every language
 *      today — `ParseWorkerResult.parsedFiles` stays empty and the legacy
 *      DAG continues unaffected.
 *   2. When a provider DOES implement the hook, the helper threads its
 *      output through `ScopeExtractor.extract` and returns a `ParsedFile`.
 *   3. Exceptions from either the hook or the extractor are caught
 *      locally. The helper returns `undefined` — scope-extraction
 *      failures must NEVER break legacy parsing on the same file.
 */

import { describe, it, expect } from 'vitest';
import type { Capture, CaptureMatch } from 'ontoindex-shared';
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
import type { LanguageProvider } from '../../../src/core/ingestion/language-provider.js';

// ─── Capture helpers ────────────────────────────────────────────────────────

const cap = (
  name: string,
  startLine: number,
  startCol: number,
  endLine: number,
  endCol: number,
  text = '',
): Capture => ({ name, range: { startLine, startCol, endLine, endCol }, text });

const moduleScopeMatch = (): CaptureMatch => ({
  '@scope.module': cap('@scope.module', 1, 0, 100, 0),
});

/**
 * Build a `LanguageProvider` whose shape is only as narrow as
 * `extractParsedFile` reads. Tests cast to the full provider type since
 * `extractParsedFile` is typed against `LanguageProvider` (not the narrow
 * `ScopeExtractorHooks`); the real worker always has a full provider.
 */
function fakeProvider(
  hooks: Partial<
    Pick<LanguageProvider, 'emitScopeCaptures' | 'shouldCreateScope' | 'resolveScopeKind'>
  >,
): LanguageProvider {
  return hooks as unknown as LanguageProvider;
}

// ─── Tests ─────────────────────────────────────────────────────────────────

describe('extractParsedFile', () => {
  describe('provider has NOT migrated (no emitScopeCaptures)', () => {
    it('returns undefined — silent no-op for legacy languages', () => {
      const provider = fakeProvider({}); // no hook
      const result = extractParsedFile(provider, 'source text', 'src/file.ts');
      expect(result).toBeUndefined();
    });

    it('never calls the scope extractor when the hook is absent — cannot throw', () => {
      // If the extractor was wrongly invoked, it would complain about the
      // missing Module scope for empty captures. This test proves the
      // short-circuit actually fires.
      const provider = fakeProvider({});
      expect(() => extractParsedFile(provider, '', 'x.ts')).not.toThrow();
    });
  });

  describe('provider HAS migrated', () => {
    it('threads emitScopeCaptures output through ScopeExtractor', () => {
      const provider = fakeProvider({
        emitScopeCaptures: () => [moduleScopeMatch()],
      });
      const result = extractParsedFile(provider, 'source text', 'src/file.ts');
      expect(result).toBeDefined();
      expect(result!.filePath).toBe('src/file.ts');
      expect(result!.scopes).toHaveLength(1);
      expect(result!.scopes[0]!.kind).toBe('Module');
    });

    it('forwards the correct arguments to emitScopeCaptures', () => {
      let seenText: string | undefined;
      let seenPath: string | undefined;
      const provider = fakeProvider({
        emitScopeCaptures: (text, path) => {
          seenText = text;
          seenPath = path;
          return [moduleScopeMatch()];
        },
      });
      extractParsedFile(provider, 'the real text', 'deep/path/file.ts');
      expect(seenText).toBe('the real text');
      expect(seenPath).toBe('deep/path/file.ts');
    });

    it('honors provider hooks beyond emitScopeCaptures (shouldCreateScope)', () => {
      // A Block scope the provider declines to create — the resulting
      // ParsedFile should have only the Module scope, not the Block.
      const provider = fakeProvider({
        emitScopeCaptures: () => [
          moduleScopeMatch(),
          { '@scope.block': cap('@scope.block', 10, 0, 20, 0) },
        ],
        shouldCreateScope: (match) => match['@scope.block'] === undefined,
      });
      const result = extractParsedFile(provider, 'src', 'a.ts');
      expect(result!.scopes).toHaveLength(1);
      expect(result!.scopes[0]!.kind).toBe('Module');
    });
  });

  describe('error resilience — never breaks legacy parsing', () => {
    it('returns undefined when emitScopeCaptures throws', () => {
      const provider = fakeProvider({
        emitScopeCaptures: () => {
          throw new Error('provider boom');
        },
      });
      const result = extractParsedFile(provider, 'src', 'a.ts');
      expect(result).toBeUndefined();
    });

    it('routes errors through the onWarn callback when provided', () => {
      const warnings: string[] = [];
      const provider = fakeProvider({
        emitScopeCaptures: () => {
          throw new Error('provider boom');
        },
      });
      const result = extractParsedFile(provider, 'src', 'path/to/file.ts', (msg) => {
        warnings.push(msg);
      });
      expect(result).toBeUndefined();
      expect(warnings).toHaveLength(1);
      expect(warnings[0]).toContain('path/to/file.ts');
      expect(warnings[0]).toContain('provider boom');
    });

    it('warns when ScopeExtractor throws an unexpected missing Module scope', () => {
      // Emits no scope captures, so the extractor reaches the missing
      // Module invariant. Legacy parsing on the same file continues
      // unaffected by this failure.
      const warnings: string[] = [];
      const provider = fakeProvider({
        emitScopeCaptures: () => [],
      });
      const result = extractParsedFile(provider, 'src', 'a.ts', (msg) => {
        warnings.push(msg);
      });
      expect(result).toBeUndefined();
      expect(warnings).toHaveLength(1);
      expect(warnings[0]).toContain('ScopeExtractor: no Module scope found');
    });

    it('suppresses expected missing Module scope warnings for LO-style .cxx files', () => {
      const warnings: string[] = [];
      const provider = fakeProvider({
        emitScopeCaptures: () => [],
      });
      const result = extractParsedFile(
        provider,
        'namespace sw { void layout() {} }',
        'sw/source/core/layout/pagechg.cxx',
        (msg) => {
          warnings.push(msg);
        },
      );
      expect(result).toBeUndefined();
      expect(warnings).toHaveLength(0);
    });

    it('suppresses expected missing Module scope warnings for LO-style .hxx files', () => {
      const warnings: string[] = [];
      const provider = fakeProvider({
        emitScopeCaptures: () => [],
      });
      const result = extractParsedFile(
        provider,
        '#pragma once\nclass SfxObjectShell;',
        'include/sfx2/objsh.hxx',
        (msg) => {
          warnings.push(msg);
        },
      );
      expect(result).toBeUndefined();
      expect(warnings).toHaveLength(0);
    });

    it('returns undefined when ScopeExtractor throws on malformed captures (overlap)', () => {
      // Siblings with overlapping ranges trip the ScopeTreeInvariantError
      // from #912. The helper catches it and returns undefined.
      const provider = fakeProvider({
        emitScopeCaptures: () => [
          moduleScopeMatch(),
          { '@scope.function': cap('@scope.function', 10, 0, 20, 0) },
          { '@scope.function': cap('@scope.function', 15, 0, 25, 0) }, // overlap
        ],
      });
      const result = extractParsedFile(provider, 'src', 'a.ts');
      expect(result).toBeUndefined();
    });

    it('still warns for unexpected scope errors in .cxx files', () => {
      const warnings: string[] = [];
      const provider = fakeProvider({
        emitScopeCaptures: () => [
          moduleScopeMatch(),
          { '@scope.function': cap('@scope.function', 10, 0, 20, 0) },
          { '@scope.function': cap('@scope.function', 15, 0, 25, 0) }, // overlap
        ],
      });
      const result = extractParsedFile(
        provider,
        'src',
        'sw/source/core/layout/pagechg.cxx',
        (msg) => {
          warnings.push(msg);
        },
      );
      expect(result).toBeUndefined();
      expect(warnings).toHaveLength(1);
      expect(warnings[0]).toContain('overlap');
    });
  });
});
