import { describe, it, expect } from 'vitest';
import {
  preprocessCobolSource,
  extractCobolSymbolsWithRegex,
} from '../../src/core/ingestion/cobol/cobol-preprocessor.js';
import { parseReplacingClause } from '../../src/core/ingestion/cobol/cobol-copy-expander.js';

// ---------------------------------------------------------------------------
// Helper: build COBOL source from an array of lines.
//
// The parser processes full raw lines including columns 1-6 (sequence area).
// Regexes anchored with ^\s+ (data items, FD, AUTHOR, etc.) require the line
// to start with whitespace, so test lines use spaces in cols 1-6 instead of
// numeric sequence numbers unless specifically testing sequence-number behavior.
//
// Column layout:
//   1-6:  sequence/patch area (spaces or digits)
//   7:    indicator (* comment, - continuation, / page break, space normal)
//   8-11: Area A (divisions, sections, paragraphs start here = 7 leading spaces)
//   12+:  Area B (statements = 11+ leading spaces)
// ---------------------------------------------------------------------------
function cobol(...lines: string[]): string {
  return lines.join('\n');
}

// ---------------------------------------------------------------------------
// preprocessCobolSource
// ---------------------------------------------------------------------------

describe('preprocessCobolSource', () => {
  it('replaces alphabetic patch markers in cols 1-6 with spaces', () => {
    const input = cobol('mzADD  IDENTIFICATION DIVISION.', 'estero PROGRAM-ID. TEST1.');
    const output = preprocessCobolSource(input);
    const lines = output.split('\n');
    expect(lines[0].substring(0, 6)).toBe('      ');
    expect(lines[0].substring(6)).toBe(' IDENTIFICATION DIVISION.');
    expect(lines[1].substring(0, 6)).toBe('      ');
  });

  it('strips numeric sequence numbers from cols 1-6', () => {
    const input = cobol('000100 IDENTIFICATION DIVISION.', '000200 PROGRAM-ID. TEST1.');
    const output = preprocessCobolSource(input);
    const lines = output.split('\n');
    expect(lines[0]).toBe('       IDENTIFICATION DIVISION.');
    expect(lines[1]).toBe('       PROGRAM-ID. TEST1.');
  });

  it('preserves lines shorter than 7 characters', () => {
    const input = cobol('SHORT', '      ', '000100 IDENTIFICATION DIVISION.');
    const output = preprocessCobolSource(input);
    const lines = output.split('\n');
    expect(lines[0]).toBe('SHORT');
    expect(lines[1]).toBe('      ');
  });

  it('preserves exact line count (no lines added/removed)', () => {
    const input = cobol(
      'mzADD  IDENTIFICATION DIVISION.',
      '000200 PROGRAM-ID. TEST1.',
      'patch# DATA DIVISION.',
      '',
      '000500 PROCEDURE DIVISION.',
    );
    const output = preprocessCobolSource(input);
    expect(output.split('\n').length).toBe(input.split('\n').length);
  });
});

// ---------------------------------------------------------------------------
// extractCobolSymbolsWithRegex
// ---------------------------------------------------------------------------

describe('extractCobolSymbolsWithRegex', () => {
  // -------------------------------------------------------------------------
  // PROGRAM-ID
  // -------------------------------------------------------------------------
  describe('PROGRAM-ID', () => {
    it('extracts PROGRAM-ID from IDENTIFICATION DIVISION', () => {
      const src = cobol('      IDENTIFICATION DIVISION.', '       PROGRAM-ID. TESTPROG.');
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('TESTPROG');
    });

    it('captures all PROGRAM-IDs in programs array with line ranges', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER-PROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           DISPLAY "OUTER".',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-PROG.',
        '      PROCEDURE DIVISION.',
        '       INNER-PARA.',
        '           DISPLAY "INNER".',
        '       END PROGRAM INNER-PROG.',
        '       END PROGRAM OUTER-PROG.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('OUTER-PROG');
      expect(r.programs).toHaveLength(2);
      expect(r.programs[0].name).toBe('OUTER-PROG');
      expect(r.programs[0].nestingDepth).toBe(0);
      expect(r.programs[1].name).toBe('INNER-PROG');
      expect(r.programs[1].nestingDepth).toBe(1);
      // INNER-PROG's startLine < endLine, contained within OUTER-PROG
      expect(r.programs[0].startLine).toBe(2); // OUTER-PROG
      expect(r.programs[1].startLine).toBe(7); // INNER-PROG
      expect(r.programs[1].endLine).toBe(11); // END PROGRAM INNER-PROG
      expect(r.programs[0].endLine).toBe(12); // END PROGRAM OUTER-PROG
    });

    it('returns null programName for content without PROGRAM-ID', () => {
      const src = cobol('      IDENTIFICATION DIVISION.', '       AUTHOR. SOMEONE.');
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBeNull();
    });
  });

  // -------------------------------------------------------------------------
  // Paragraphs & Sections
  // -------------------------------------------------------------------------
  describe('Paragraphs & Sections', () => {
    it('extracts paragraphs in PROCEDURE DIVISION (7 leading spaces)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           DISPLAY "HELLO".',
        '       SUB-PARA.',
        '           DISPLAY "WORLD".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.paragraphs).toHaveLength(2);
      expect(r.paragraphs[0].name).toBe('MAIN-PARA');
      expect(r.paragraphs[1].name).toBe('SUB-PARA');
    });

    it('extracts sections in PROCEDURE DIVISION', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       INIT-SECTION SECTION.',
        '       INIT-PARA.',
        '           DISPLAY "INIT".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sections).toHaveLength(1);
      expect(r.sections[0].name).toBe('INIT-SECTION');
      expect(r.paragraphs).toHaveLength(1);
      expect(r.paragraphs[0].name).toBe('INIT-PARA');
    });

    it('excludes reserved names (DECLARATIVES, END, PROCEDURE, etc.)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       DECLARATIVES.',
        '       END.',
        '       REAL-PARA.',
        '           DISPLAY "OK".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.paragraphs.map((p) => p.name)).toEqual(['REAL-PARA']);
    });

    it('does NOT treat IDENTIFICATION/ENVIRONMENT/DATA/WORKING-STORAGE as paragraphs', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '      PROCEDURE DIVISION.',
        '       REAL-PARA.',
        '           DISPLAY "OK".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const names = r.paragraphs.map((p) => p.name);
      expect(names).not.toContain('IDENTIFICATION');
      expect(names).not.toContain('ENVIRONMENT');
      expect(names).not.toContain('DATA');
      expect(names).not.toContain('WORKING-STORAGE');
      expect(names).toContain('REAL-PARA');
    });
  });

  // -------------------------------------------------------------------------
  // CALL / PERFORM / COPY
  // -------------------------------------------------------------------------
  describe('CALL / PERFORM / COPY', () => {
    it('extracts CALL "PROGRAM" statements', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CALL "SUBPROG".',
        '           CALL "ANOTHER".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(2);
      expect(r.calls[0].target).toBe('SUBPROG');
      expect(r.calls[1].target).toBe('ANOTHER');
    });

    it('extracts PERFORM paragraph-name with caller context', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           PERFORM SUB-PARA.',
        '       SUB-PARA.',
        '           DISPLAY "HELLO".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.performs).toHaveLength(1);
      expect(r.performs[0].target).toBe('SUB-PARA');
      expect(r.performs[0].caller).toBe('MAIN-PARA');
    });

    it('extracts PERFORM ... THRU ... statements', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           PERFORM STEP-A THRU STEP-Z.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.performs).toHaveLength(1);
      expect(r.performs[0].target).toBe('STEP-A');
      expect(r.performs[0].thruTarget).toBe('STEP-Z');
    });

    it('does NOT store PERFORM WS-COUNT TIMES as a perform target', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           PERFORM WS-COUNT TIMES.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.performs.map((p) => p.target)).not.toContain('WS-COUNT');
    });

    it('extracts dynamic CALL (unquoted) with isQuoted=false', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CALL WS-PROG-NAME.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('WS-PROG-NAME');
      expect(r.calls[0].isQuoted).toBe(false);
    });

    it('quoted CALL has isQuoted=true', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CALL "SUBPROG".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].isQuoted).toBe(true);
    });

    it('extracts COPY copybook (unquoted)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '           COPY WSCOPY.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.copies).toHaveLength(1);
      expect(r.copies[0].target).toBe('WSCOPY');
    });

    it('extracts COPY "copybook" (quoted)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '           COPY "MY-COPY".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.copies).toHaveLength(1);
      expect(r.copies[0].target).toBe('MY-COPY');
    });
  });

  // -------------------------------------------------------------------------
  // Data Division
  // -------------------------------------------------------------------------
  describe('Data Division', () => {
    it('extracts data items with level, name, PIC, USAGE', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01  WS-RECORD.',
        '           05  WS-NAME          PIC X(30).',
        '           05  WS-AMOUNT        PIC 9(7)V99 USAGE COMP-3.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.length).toBe(3); // WS-NAME + WS-BALANCE + WS-AMOUNT (01-level group with only period has no clauses)

      const wsName = r.dataItems.find((d) => d.name === 'WS-NAME');
      expect(wsName).toBeDefined();
      expect(wsName!.level).toBe(5);
      expect(wsName!.pic).toMatch(/^X\(30\)/);

      const wsAmount = r.dataItems.find((d) => d.name === 'WS-AMOUNT');
      expect(wsAmount).toBeDefined();
      expect(wsAmount!.usage).toBe('COMP-3');
    });

    it('extracts 88-level condition names with values', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01  WS-STATUS          PIC X.',
        '           88  WS-ACTIVE      VALUE "A".',
        '           88  WS-INACTIVE    VALUE "I".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const active = r.dataItems.find((d) => d.name === 'WS-ACTIVE');
      expect(active).toBeDefined();
      expect(active!.level).toBe(88);
      expect(active!.values).toEqual(['A']);

      const inactive = r.dataItems.find((d) => d.name === 'WS-INACTIVE');
      expect(inactive).toBeDefined();
      expect(inactive!.values).toEqual(['I']);
    });

    it('extracts FD entries with record name linkage', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      FILE SECTION.',
        '       FD  EMPLOYEE-FILE.',
        '       01  EMPLOYEE-RECORD.',
        '           05  EMP-ID          PIC 9(5).',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.fdEntries).toHaveLength(1);
      expect(r.fdEntries[0].fdName).toBe('EMPLOYEE-FILE');
      expect(r.fdEntries[0].recordName).toBe('EMPLOYEE-RECORD');
    });

    it('skips FILLER items', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01  WS-REC.',
        '           05  FILLER            PIC X(10).',
        '           05  WS-DATA           PIC X(20).',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const fillerItems = r.dataItems.filter((d) => d.name === 'FILLER');
      expect(fillerItems).toHaveLength(0);
      expect(r.dataItems.find((d) => d.name === 'WS-DATA')).toBeDefined();
    });

    it('correctly assigns data section (working-storage, linkage, file)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      FILE SECTION.',
        '       FD  MY-FILE.',
        '       01  FILE-REC              PIC X(80).',
        '      WORKING-STORAGE SECTION.',
        '       01  WS-VAR               PIC X(10).',
        '      LINKAGE SECTION.',
        '       01  LK-VAR               PIC X(10).',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');

      const fileRec = r.dataItems.find((d) => d.name === 'FILE-REC');
      expect(fileRec).toBeDefined();
      expect(fileRec!.section).toBe('file');

      const wsVar = r.dataItems.find((d) => d.name === 'WS-VAR');
      expect(wsVar).toBeDefined();
      expect(wsVar!.section).toBe('working-storage');

      const lkVar = r.dataItems.find((d) => d.name === 'LK-VAR');
      expect(lkVar).toBeDefined();
      expect(lkVar!.section).toBe('linkage');
    });
  });

  // -------------------------------------------------------------------------
  // Environment Division
  // -------------------------------------------------------------------------
  describe('Environment Division', () => {
    it('extracts SELECT ... ASSIGN TO with organization, access, record key', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      INPUT-OUTPUT SECTION.',
        '       FILE-CONTROL.',
        '           SELECT EMPLOYEE-FILE',
        '               ASSIGN TO "EMPFILE"',
        '               ORGANIZATION IS INDEXED',
        '               ACCESS MODE IS DYNAMIC',
        '               RECORD KEY IS EMP-ID.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.fileDeclarations).toHaveLength(1);
      const fd = r.fileDeclarations[0];
      expect(fd.selectName).toBe('EMPLOYEE-FILE');
      expect(fd.assignTo).toBe('EMPFILE');
      expect(fd.organization).toBe('INDEXED');
      expect(fd.access).toBe('DYNAMIC');
      expect(fd.recordKey).toBe('EMP-ID');
    });
  });

  // -------------------------------------------------------------------------
  // State Machine
  // -------------------------------------------------------------------------
  describe('State Machine', () => {
    it('correctly transitions between divisions', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01  WS-VAR              PIC X(10).',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           DISPLAY WS-VAR.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('TESTPROG');
      expect(r.dataItems.find((d) => d.name === 'WS-VAR')).toBeDefined();
      expect(r.paragraphs).toHaveLength(1);
      expect(r.paragraphs[0].name).toBe('MAIN-PARA');
    });

    it('handles continuation lines (indicator "-" in column 7)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CALL "VERY-LONG-PR',
        '      -    "OGRAM-NAME".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // Continuation merges lines; at minimum verify no crash and paragraph found
      expect(r.paragraphs).toHaveLength(1);
      expect(r.paragraphs[0].name).toBe('MAIN-PARA');
    });

    it('skips comment lines (indicator "*" or "/" in column 7)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '      *    THIS IS A COMMENT',
        '      /    THIS IS A PAGE BREAK COMMENT',
        '           CALL "REALPROG".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('REALPROG');
    });
  });

  // -------------------------------------------------------------------------
  // EXEC Blocks
  // -------------------------------------------------------------------------
  describe('EXEC Blocks', () => {
    it('extracts EXEC SQL blocks with tables and host variables', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL',
        '             SELECT EMP-NAME, EMP-SALARY',
        '             FROM EMPLOYEE',
        '             WHERE EMP-ID = :WS-EMP-ID',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks).toHaveLength(1);
      const sql = r.execSqlBlocks[0];
      expect(sql.operation).toBe('SELECT');
      expect(sql.tables).toContain('EMPLOYEE');
      expect(sql.hostVariables).toContain('WS-EMP-ID');
    });

    it('extracts EXEC CICS blocks with command and MAP/PROGRAM/TRANSID', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           EXEC CICS SEND MAP('EMPMAP')",
        "             PROGRAM('EMPPROG')",
        "             TRANSID('EMPT')",
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execCicsBlocks).toHaveLength(1);
      const cics = r.execCicsBlocks[0];
      expect(cics.command).toBe('SEND MAP');
      expect(cics.mapName).toBe('EMPMAP');
      expect(cics.programName).toBe('EMPPROG');
      expect(cics.transId).toBe('EMPT');
    });

    it('extracts EXEC CICS MAP with unquoted identifier', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC CICS SEND MAP(WS-MAP-NAME)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execCicsBlocks).toHaveLength(1);
      expect(r.execCicsBlocks[0].mapName).toBe('WS-MAP-NAME');
    });

    it('handles single-line EXEC SQL ... END-EXEC', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL DELETE FROM ORDERS WHERE ORD-ID = :WS-ORD END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks).toHaveLength(1);
      expect(r.execSqlBlocks[0].operation).toBe('DELETE');
      expect(r.execSqlBlocks[0].tables).toContain('ORDERS');
    });

    it('handles multi-line EXEC SQL ... END-EXEC', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL',
        '             INSERT INTO AUDIT_LOG',
        '             VALUES (:WS-TIMESTAMP, :WS-USER)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks).toHaveLength(1);
      const sql = r.execSqlBlocks[0];
      expect(sql.operation).toBe('INSERT');
      expect(sql.tables).toContain('AUDIT_LOG');
      expect(sql.hostVariables).toContain('WS-TIMESTAMP');
      expect(sql.hostVariables).toContain('WS-USER');
    });
  });

  // -------------------------------------------------------------------------
  // Linkage & Data Flow
  // -------------------------------------------------------------------------
  describe('Linkage & Data Flow', () => {
    it('extracts PROCEDURE DIVISION USING parameters', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      LINKAGE SECTION.',
        '       01  LK-PARAM1            PIC X(10).',
        '       01  LK-PARAM2            PIC 9(5).',
        '      PROCEDURE DIVISION USING LK-PARAM1 LK-PARAM2.',
        '       MAIN-PARA.',
        '           DISPLAY LK-PARAM1.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.procedureUsing).toEqual(['LK-PARAM1', 'LK-PARAM2']);
    });

    it('extracts ENTRY points with USING', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           ENTRY "ALTENTRY" USING WS-PARAM1 WS-PARAM2.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.entryPoints).toHaveLength(1);
      expect(r.entryPoints[0].name).toBe('ALTENTRY');
      expect(r.entryPoints[0].parameters).toEqual(['WS-PARAM1', 'WS-PARAM2']);
    });

    it("extracts ENTRY 'ALTENTRY' with single-quoted target", () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           ENTRY 'ALTENTRY' USING WS-PARAM1.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.entryPoints).toHaveLength(1);
      expect(r.entryPoints[0].name).toBe('ALTENTRY');
      expect(r.entryPoints[0].parameters).toEqual(['WS-PARAM1']);
    });

    it('ENTRY USING filters calling-convention keywords (BY VALUE REFERENCE)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           ENTRY 'ALTENTRY' USING BY VALUE WS-AMT BY REFERENCE LS-REC.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.entryPoints).toHaveLength(1);
      // BY, VALUE, REFERENCE should be filtered out — only actual parameter names remain
      expect(r.entryPoints[0].parameters).toEqual(['WS-AMT', 'LS-REC']);
    });

    it('paragraphs with SECTION in name are NOT excluded (e.g., CROSS-SECTION-PROC)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       CROSS-SECTION-ANALYSIS.',
        '           DISPLAY "HELLO".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.paragraphs.map((p) => p.name)).toContain('CROSS-SECTION-ANALYSIS');
    });

    it('PERFORM THROUGH (full spelling) captures thruTarget', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           PERFORM FIRST-PARA THROUGH LAST-PARA.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.performs).toHaveLength(1);
      expect(r.performs[0].target).toBe('FIRST-PARA');
      expect(r.performs[0].thruTarget).toBe('LAST-PARA');
    });

    it('PROCEDURE DIVISION USING RETURNING excludes return value from USING list', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION USING WS-INPUT RETURNING WS-RESULT.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // RETURNING and everything after it should be excluded — only USING parameters remain
      expect(r.procedureUsing).toEqual(['WS-INPUT']);
    });

    it('RE_CALL_DYNAMIC does NOT false-match on WS-CALL compound identifier', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       05  WS-CALL OCCURS 10 PIC X(10).',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           DISPLAY WS-CALL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // WS-CALL should NOT produce a dynamic CALL — it's a data item name
      expect(r.calls.filter((c) => !c.isQuoted)).toHaveLength(0);
    });

    it('multi-line SORT captures USING and GIVING from continuation lines', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SORT SORT-FILE',
        '               ON ASCENDING KEY WS-KEY',
        '               USING INPUT-FILE',
        '               GIVING OUTPUT-FILE.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sorts).toHaveLength(1);
      expect(r.sorts[0].sortFile).toBe('SORT-FILE');
      expect(r.sorts[0].usingFiles).toContain('INPUT-FILE');
      expect(r.sorts[0].givingFiles).toContain('OUTPUT-FILE');
    });

    it('PROCEDURE DIVISION USING on split line is captured via pendingProcUsing', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION',
        '           USING WS-PARAM1 WS-PARAM2.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.procedureUsing).toEqual(['WS-PARAM1', 'WS-PARAM2']);
    });

    it('nested programs carry per-program procedureUsing', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION USING WS-OUTER-PARAM.',
        '       MAIN-PARA.',
        '           DISPLAY WS-OUTER-PARAM.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER.',
        '      PROCEDURE DIVISION USING WS-INNER-PARAM.',
        '       INNER-PARA.',
        '           DISPLAY WS-INNER-PARAM.',
        '       END PROGRAM INNER.',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programs).toHaveLength(2);
      const outer = r.programs.find((p) => p.name === 'OUTER');
      const inner = r.programs.find((p) => p.name === 'INNER');
      expect(outer?.procedureUsing).toEqual(['WS-OUTER-PARAM']);
      expect(inner?.procedureUsing).toEqual(['WS-INNER-PARAM']);
    });

    it('SECTION with segment number is detected', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-SECTION SECTION 30.',
        '       MAIN-PARA.',
        '           DISPLAY "HI".',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sections.map((s) => s.name)).toContain('MAIN-SECTION');
    });

    it('dynamic CANCEL via data item is captured with isQuoted=false', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CANCEL WS-PGM-NAME.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.cancels).toHaveLength(1);
      expect(r.cancels[0].target).toBe('WS-PGM-NAME');
      expect(r.cancels[0].isQuoted).toBe(false);
    });

    it('copybook preprocessing strips sequence numbers before expansion', () => {
      // This is tested indirectly — preprocessCobolSource is called in readCopy
      const input = cobol('000100 IDENTIFICATION DIVISION.', '000200 PROGRAM-ID. TEST1.');
      const output = preprocessCobolSource(input);
      // Verify cols 1-6 are blanked for numeric sequences
      expect(output.split('\n')[0]).toBe('       IDENTIFICATION DIVISION.');
    });

    it('numeric sequence numbers are stripped so paragraphs are detected', () => {
      const src = preprocessCobolSource(
        cobol(
          '000100 IDENTIFICATION DIVISION.',
          '000200 PROGRAM-ID. SEQTEST.',
          '000300 PROCEDURE DIVISION.',
          '000400 MAIN-PARA.',
          '000500     DISPLAY "HI".',
        ),
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('SEQTEST');
      expect(r.paragraphs.map((p) => p.name)).toEqual(['MAIN-PARA']);
    });

    it('extracts MOVE statements (skipping figurative constants)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           MOVE WS-SOURCE TO WS-TARGET.',
        '           MOVE SPACES TO WS-BLANK.',
        '           MOVE ZEROS TO WS-ZERO.',
        '           MOVE CORRESPONDING WS-REC1 TO WS-REC2.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const moveData = r.moves.map((m) => ({
        from: m.from,
        targets: m.targets,
        corr: m.corresponding,
      }));
      expect(moveData).toContainEqual({ from: 'WS-SOURCE', targets: ['WS-TARGET'], corr: false });
      expect(moveData).toContainEqual({ from: 'WS-REC1', targets: ['WS-REC2'], corr: true });
      expect(r.moves.find((m) => m.from === 'SPACES')).toBeUndefined();
      expect(r.moves.find((m) => m.from === 'ZEROS')).toBeUndefined();
    });

    it('captures multiple MOVE targets: MOVE X TO A B C', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           MOVE WS-SOURCE TO WS-A WS-B WS-C.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.moves).toHaveLength(1);
      expect(r.moves[0].targets).toEqual(['WS-A', 'WS-B', 'WS-C']);
    });

    it('MOVE CORRESPONDING is always single target', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           MOVE CORRESPONDING WS-REC1 TO WS-REC2.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.moves).toHaveLength(1);
      expect(r.moves[0].targets).toEqual(['WS-REC2']);
      expect(r.moves[0].corresponding).toBe(true);
    });

    it('MOVE handles OF-qualified names', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           MOVE WS-SRC TO WS-NAME OF WS-RECORD WS-CODE.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.moves).toHaveLength(1);
      // WS-NAME OF WS-RECORD -> WS-NAME is the target; WS-CODE is a second target
      expect(r.moves[0].targets).toEqual(['WS-NAME', 'WS-CODE']);
    });

    it('MOVE skips figurative constants in targets', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           MOVE WS-SRC TO SPACES.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // SPACES is in MOVE_SKIP, so no targets -> no move entry
      expect(r.moves).toHaveLength(0);
    });
  });

  // -------------------------------------------------------------------------
  // Edge Cases
  // -------------------------------------------------------------------------
  describe('Edge Cases', () => {
    it('empty program returns empty results', () => {
      const r = extractCobolSymbolsWithRegex('', 'empty.cbl');
      expect(r.programName).toBeNull();
      expect(r.paragraphs).toHaveLength(0);
      expect(r.sections).toHaveLength(0);
      expect(r.performs).toHaveLength(0);
      expect(r.calls).toHaveLength(0);
      expect(r.copies).toHaveLength(0);
      expect(r.dataItems).toHaveLength(0);
      expect(r.fileDeclarations).toHaveLength(0);
      expect(r.fdEntries).toHaveLength(0);
      expect(r.execSqlBlocks).toHaveLength(0);
      expect(r.execCicsBlocks).toHaveLength(0);
      expect(r.procedureUsing).toHaveLength(0);
      expect(r.entryPoints).toHaveLength(0);
      expect(r.moves).toHaveLength(0);
    });

    it('extracts AUTHOR and DATE-WRITTEN from program metadata', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '       AUTHOR. JOHN DOE.',
        '       DATE-WRITTEN. 2025-01-15.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programMetadata.author).toBe('JOHN DOE');
      expect(r.programMetadata.dateWritten).toBe('2025-01-15');
    });
  });

  // -------------------------------------------------------------------------
  // Phase 1: Data Flow Features
  // -------------------------------------------------------------------------
  describe('Phase 1: Data Flow Features', () => {
    it('EXEC SQL INCLUDE extracts member name (unquoted)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '           EXEC SQL INCLUDE SQLCA END-EXEC.',
        '           EXEC SQL INCLUDE CUSTDCL END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const includes = r.execSqlBlocks.filter((b) => b.includeMember);
      expect(includes).toHaveLength(2);
      expect(includes[0].includeMember).toBe('SQLCA');
      expect(includes[1].includeMember).toBe('CUSTDCL');
    });

    it('EXEC SQL INCLUDE handles quoted and underscored member names', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        "           EXEC SQL INCLUDE 'DBRMLIB.MEMBER' END-EXEC.",
        '           EXEC SQL INCLUDE CUST_TBL_DCL END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const includes = r.execSqlBlocks.filter((b) => b.includeMember);
      expect(includes).toHaveLength(2);
      expect(includes[0].includeMember).toBe('DBRMLIB.MEMBER');
      expect(includes[1].includeMember).toBe('CUST_TBL_DCL');
    });

    it('CALL USING extracts parameters', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'AUDITLOG' USING WS-CUST-ID WS-AMOUNT.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-CUST-ID', 'WS-AMOUNT']);
    });

    it('CALL USING filters BY REFERENCE/CONTENT/VALUE keywords', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM' USING BY REFERENCE WS-A BY CONTENT WS-B BY VALUE WS-C.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls[0].parameters).toEqual(['WS-A', 'WS-B', 'WS-C']);
    });

    it('CALL USING filters ADDRESS OF and OMITTED', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM' USING ADDRESS OF WS-REC OMITTED WS-FLAG.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls[0].parameters).toEqual(['WS-REC', 'WS-FLAG']);
    });

    it('CALL RETURNING extracts return target', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'FUNC' USING WS-INPUT RETURNING WS-RESULT.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls[0].parameters).toEqual(['WS-INPUT']);
      expect(r.calls[0].returning).toBe('WS-RESULT');
    });

    it('OCCURS DEPENDING ON captures controlling field', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-COUNT PIC 9(4).',
        '       01 WS-TABLE OCCURS 1 TO 100 DEPENDING ON WS-COUNT.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const table = r.dataItems.find((d) => d.name === 'WS-TABLE');
      expect(table).toBeDefined();
      expect(table!.dependingOn).toBe('WS-COUNT');
      expect(table!.occurs).toBe(1);
    });

    it('VALUE clause extracts quoted string', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        "       01 WS-STATUS PIC X VALUE 'A'.",
        '       01 WS-COUNT PIC 9(4) VALUE 0.',
        '       01 WS-NAME PIC X(10) VALUE SPACES.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.find((d) => d.name === 'WS-STATUS')?.values).toEqual(['A']);
      expect(r.dataItems.find((d) => d.name === 'WS-COUNT')?.values).toEqual(['0']);
      expect(r.dataItems.find((d) => d.name === 'WS-NAME')?.values).toEqual(['SPACES']);
    });
  });

  // -------------------------------------------------------------------------
  // Phase 2: IMS + Error Handling Features
  // -------------------------------------------------------------------------
  describe('Phase 2: IMS + Error Handling Features', () => {
    it('EXEC DLI GU extracts verb, segment, PCB, and INTO', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC DLI GU USING PCB(2)',
        '               SEGMENT(CUSTOMER)',
        '               INTO(CUST-IO-AREA)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execDliBlocks).toHaveLength(1);
      expect(r.execDliBlocks[0].verb).toBe('GU');
      expect(r.execDliBlocks[0].pcbNumber).toBe(2);
      expect(r.execDliBlocks[0].segmentName).toBe('CUSTOMER');
      expect(r.execDliBlocks[0].intoField).toBe('CUST-IO-AREA');
    });

    it('EXEC DLI ISRT extracts FROM field', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC DLI ISRT USING PCB(1)',
        '               SEGMENT(ORDER)',
        '               FROM(ORDER-IO-AREA)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execDliBlocks[0].verb).toBe('ISRT');
      expect(r.execDliBlocks[0].fromField).toBe('ORDER-IO-AREA');
    });

    it('EXEC DLI SCHD extracts PSB name', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC DLI SCHD PSB(CUSTPSB) END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execDliBlocks[0].verb).toBe('SCHD');
      expect(r.execDliBlocks[0].psbName).toBe('CUSTPSB');
    });

    it('DECLARATIVES USE AFTER EXCEPTION extracts file binding', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '      DECLARATIVES.',
        '      CUST-ERR SECTION.',
        '          USE AFTER STANDARD ERROR ON CUSTOMER-FILE.',
        '       CUST-ERR-PARA.',
        '           DISPLAY "FILE ERROR".',
        '      END DECLARATIVES.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.declaratives).toHaveLength(1);
      expect(r.declaratives[0].sectionName).toBe('CUST-ERR');
      expect(r.declaratives[0].target).toBe('CUSTOMER-FILE');
    });

    it('DECLARATIVES with multiple USE sections', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '      DECLARATIVES.',
        '      ERR-A SECTION.',
        '          USE AFTER STANDARD EXCEPTION ON FILE-A.',
        '       ERR-A-PARA.',
        '           DISPLAY "A".',
        '      ERR-B SECTION.',
        '          USE AFTER STANDARD EXCEPTION ON INPUT.',
        '       ERR-B-PARA.',
        '           DISPLAY "B".',
        '      END DECLARATIVES.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.declaratives).toHaveLength(2);
      expect(r.declaratives[0].target).toBe('FILE-A');
      expect(r.declaratives[1].target).toBe('INPUT');
    });

    it('SET condition TO TRUE extracts targets', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SET END-OF-FILE TO TRUE.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].form).toBe('to-true');
      expect(r.sets[0].targets).toEqual(['END-OF-FILE']);
    });

    it('SET index UP BY extracts target and value', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SET IDX-1 UP BY 1.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].form).toBe('up-by');
      expect(r.sets[0].targets).toEqual(['IDX-1']);
      expect(r.sets[0].value).toBe('1');
    });

    it('INSPECT TALLYING extracts field and counter', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           INSPECT WS-STRING TALLYING WS-COUNT FOR ALL 'A'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].inspectedField).toBe('WS-STRING');
      expect(r.inspects[0].counters).toEqual(['WS-COUNT']);
      expect(r.inspects[0].form).toBe('tallying');
    });

    it('INSPECT REPLACING detected', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           INSPECT WS-FIELD REPLACING ALL 'A' BY 'B'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].form).toBe('replacing');
    });
  });

  // -------------------------------------------------------------------------
  // Phase 3-4: Completeness + Niche Features
  // -------------------------------------------------------------------------
  describe('Phase 3-4: Completeness + Niche Features', () => {
    it('SELECT OPTIONAL sets isOptional flag', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      INPUT-OUTPUT SECTION.',
        '      FILE-CONTROL.',
        "          SELECT OPTIONAL CUST-FILE ASSIGN TO 'CUSTFILE'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.fileDeclarations).toHaveLength(1);
      expect(r.fileDeclarations[0].selectName).toBe('CUST-FILE');
      expect(r.fileDeclarations[0].isOptional).toBe(true);
    });

    it('ALTERNATE RECORD KEY extraction', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      INPUT-OUTPUT SECTION.',
        '      FILE-CONTROL.',
        "          SELECT CUST-FILE ASSIGN TO 'CUSTFILE'",
        '              RECORD KEY IS CUST-ID',
        '              ALTERNATE RECORD KEY IS CUST-NAME.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.fileDeclarations[0].recordKey).toBe('CUST-ID');
      expect(r.fileDeclarations[0].alternateKeys).toEqual(['CUST-NAME']);
    });

    it('PROGRAM-ID IS COMMON sets isCommon flag', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER-PGM.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           STOP RUN.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-PGM IS COMMON.',
        '      PROCEDURE DIVISION.',
        '       INNER-PARA.',
        '           STOP RUN.',
        '       END PROGRAM INNER-PGM.',
        '       END PROGRAM OUTER-PGM.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const inner = r.programs.find((p) => p.name === 'INNER-PGM');
      expect(inner).toBeDefined();
      expect(inner!.isCommon).toBe(true);
      const outer = r.programs.find((p) => p.name === 'OUTER-PGM');
      expect(outer!.isCommon).toBeFalsy();
    });

    it('IS EXTERNAL and IS GLOBAL as boolean properties', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-SHARED PIC X(10) IS EXTERNAL.',
        '       01 WS-GLOBAL PIC X(10) IS GLOBAL.',
        '       01 WS-NORMAL PIC X(10).',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.find((d) => d.name === 'WS-SHARED')?.isExternal).toBe(true);
      expect(r.dataItems.find((d) => d.name === 'WS-GLOBAL')?.isGlobal).toBe(true);
      expect(r.dataItems.find((d) => d.name === 'WS-NORMAL')?.isExternal).toBeUndefined();
      expect(r.dataItems.find((d) => d.name === 'WS-NORMAL')?.isGlobal).toBeUndefined();
    });

    it('INITIALIZE extracts target', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INITIALIZE WS-RECORD.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.initializes).toHaveLength(1);
      expect(r.initializes[0].target).toBe('WS-RECORD');
    });

    it('AUTHOR and DATE-WRITTEN mapped to programMetadata', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '       AUTHOR. JOHN DOE.',
        '       DATE-WRITTEN. 2026-03-26.',
        '       DATE-COMPILED. 2026-03-26.',
        '       INSTALLATION. MAINFRAME-01.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programMetadata.author).toBe('JOHN DOE');
      expect(r.programMetadata.dateWritten).toBe('2026-03-26');
      expect(r.programMetadata.dateCompiled).toBe('2026-03-26');
      expect(r.programMetadata.installation).toBe('MAINFRAME-01');
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: Multi-line CALL USING accumulation
  // -------------------------------------------------------------------------
  describe('Multi-line CALL USING accumulation', () => {
    it('captures USING parameters on separate lines (IBM mainframe style)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'CUSTUPDT'",
        '               USING BY REFERENCE WS-CUST-ID',
        '                                  WS-CUST-NAME',
        '                                  WS-CUST-ADDR.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('CUSTUPDT');
      expect(r.calls[0].parameters).toEqual(['WS-CUST-ID', 'WS-CUST-NAME', 'WS-CUST-ADDR']);
    });

    it('does NOT absorb next statement as USING parameter (no END-CALL)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'CUSTUPDT'",
        '               USING WS-PARM.',
        '           INSPECT WS-STATUS TALLYING WS-CNT FOR ALL SPACES.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-PARM']);
      // INSPECT should be extracted separately, not absorbed
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].inspectedField).toBe('WS-STATUS');
    });

    it('does NOT absorb GO TO on next line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'CUSTUPDT'",
        '               USING WS-PARM.',
        '           GO TO EXIT-PARAGRAPH.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('CUSTUPDT');
      expect(r.gotos).toHaveLength(1);
      expect(r.gotos[0].target).toBe('EXIT-PARAGRAPH');
    });

    it('does NOT create false paragraph from last USING parameter on own line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-A',
        '                     WS-B.',
        '           PERFORM NEXT-PARA.',
        '       NEXT-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // WS-B should NOT be a paragraph
      const paraNames = r.paragraphs.map((p) => p.name);
      expect(paraNames).toContain('MAIN-PARA');
      expect(paraNames).toContain('NEXT-PARA');
      expect(paraNames).not.toContain('WS-B');
      // WS-B should be captured as USING parameter
      expect(r.calls[0].parameters).toContain('WS-B');
    });

    it('handles CALL with END-CALL scope terminator', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM' USING WS-A",
        '               ON EXCEPTION',
        '                   DISPLAY "ERROR"',
        '           END-CALL',
        '           PERFORM NEXT-STEP.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-A']);
      expect(r.performs).toHaveLength(1);
      expect(r.performs[0].target).toBe('NEXT-STEP');
    });

    it('does NOT false-flush on hyphenated identifiers like MOVE-COUNT', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING MOVE-COUNT',
        '                     PERFORM-LIMIT',
        '                     READ-STATUS.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls[0].parameters).toEqual(['MOVE-COUNT', 'PERFORM-LIMIT', 'READ-STATUS']);
    });

    it('captures both quoted and dynamic CALL on same line (ON EXCEPTION)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PRIMARY' ON EXCEPTION CALL WS-FALLBACK.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(2);
      expect(r.calls[0].target).toBe('PRIMARY');
      expect(r.calls[0].isQuoted).toBe(true);
      expect(r.calls[1].target).toBe('WS-FALLBACK');
      expect(r.calls[1].isQuoted).toBe(false);
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: Nested program edge attribution
  // -------------------------------------------------------------------------
  describe('Nested program edge attribution', () => {
    it('CALL in inner nested program attributed to inner module (not outer)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER-PGM.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        '           STOP RUN.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-PGM.',
        '      PROCEDURE DIVISION.',
        '       INNER-MAIN.',
        "           CALL 'SUBPROG'.",
        '       END PROGRAM INNER-PGM.',
        '       END PROGRAM OUTER-PGM.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // The CALL should have line number within INNER-PGM's range
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('SUBPROG');
      const innerProg = r.programs.find((p) => p.name === 'INNER-PGM');
      expect(innerProg).toBeDefined();
      expect(r.calls[0].line).toBe(10); // Line 10 in the fixture: CALL 'SUBPROG'.
    });

    it('PERFORM before first paragraph in nested program has correct caller', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER-PGM.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        '           STOP RUN.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-PGM.',
        '      PROCEDURE DIVISION.',
        '           PERFORM INNER-INIT.',
        '       INNER-INIT.',
        '           STOP RUN.',
        '       END PROGRAM INNER-PGM.',
        '       END PROGRAM OUTER-PGM.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // PERFORM before first paragraph — caller should be null (module-level)
      const innerPerform = r.performs.find((p) => p.target === 'INNER-INIT');
      expect(innerPerform).toBeDefined();
      expect(innerPerform!.caller).toBeNull();
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: CRLF / Windows line ending compatibility
  // -------------------------------------------------------------------------
  describe('CRLF / Windows line ending compatibility', () => {
    it('GO TO DEPENDING ON works with CRLF line endings', () => {
      // Simulate CRLF by using \r\n
      const src = [
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           GO TO PARA-A PARA-B PARA-C',
        '               DEPENDING ON WS-SWITCH.',
      ].join('\r\n');
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.gotos).toHaveLength(3);
      expect(r.gotos.map((g) => g.target).sort()).toEqual(['PARA-A', 'PARA-B', 'PARA-C']);
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: Fixed-format Area A paragraph detection
  // -------------------------------------------------------------------------
  describe('Fixed-format Area A paragraph detection', () => {
    it('rejects deeply-indented identifiers as paragraphs (Area B)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '               WS-CUST-ADDR.', // Area B (>7 spaces) — NOT a paragraph
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const paraNames = r.paragraphs.map((p) => p.name);
      expect(paraNames).toContain('MAIN-PARA');
      expect(paraNames).not.toContain('WS-CUST-ADDR');
    });

    it('accepts Area A indented paragraphs (7 spaces)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       REAL-PARA.', // 7 spaces — Area A, valid paragraph
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.paragraphs.map((p) => p.name)).toContain('REAL-PARA');
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: SORT/MERGE edge cases
  // -------------------------------------------------------------------------
  describe('SORT/MERGE edge cases', () => {
    it('captures SORT GIVING without spurious COLLATING SEQUENCE keywords', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SORT SORT-FILE ON ASCENDING KEY SORT-KEY',
        '               COLLATING SEQUENCE IS NATL',
        '               USING INPUT-FILE',
        '               GIVING OUTPUT-FILE.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sorts).toHaveLength(1);
      expect(r.sorts[0].usingFiles).toEqual(['INPUT-FILE']);
      // COLLATING, SEQUENCE, IS, NATL should NOT appear as giving files
      expect(r.sorts[0].givingFiles).toEqual(['OUTPUT-FILE']);
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: PROCEDURE DIVISION USING edge cases
  // -------------------------------------------------------------------------
  describe('PROCEDURE DIVISION USING edge cases', () => {
    it('excludes RETURNING value from USING parameter list', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION USING WS-INPUT RETURNING WS-RESULT.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.procedureUsing).toEqual(['WS-INPUT']);
    });

    it('pendingProcUsing not set for period-terminated PROCEDURE DIVISION', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.procedureUsing).toEqual([]);
      // No spurious parameters from the first procedure line
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: Comment stripping edge cases
  // -------------------------------------------------------------------------
  describe('Comment stripping edge cases', () => {
    it('pipe character inside quoted string is preserved (not treated as comment)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        "       01 WS-SEP PIC X VALUE '|'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // The data item should be extracted (not truncated by pipe)
      expect(r.dataItems.find((d) => d.name === 'WS-SEP')).toBeDefined();
    });
  });

  // -------------------------------------------------------------------------
  // Reviews 9-15: SELECT OPTIONAL and ALTERNATE KEY
  // -------------------------------------------------------------------------
  describe('SELECT OPTIONAL and ALTERNATE KEY', () => {
    it('SELECT OPTIONAL captures correct file name (not OPTIONAL keyword)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      ENVIRONMENT DIVISION.',
        '      INPUT-OUTPUT SECTION.',
        '      FILE-CONTROL.',
        "          SELECT OPTIONAL BACKUP-FILE ASSIGN TO 'BACKUP'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.fileDeclarations).toHaveLength(1);
      expect(r.fileDeclarations[0].selectName).toBe('BACKUP-FILE');
      expect(r.fileDeclarations[0].isOptional).toBe(true);
    });
  });

  // -------------------------------------------------------------------------
  // Regression: EXEC DLI edge cases
  // -------------------------------------------------------------------------
  describe('EXEC DLI edge cases', () => {
    it('EXEC DLI without SEGMENT clause (DLET/REPL)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC DLI DLET USING PCB(2) END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execDliBlocks).toHaveLength(1);
      expect(r.execDliBlocks[0].verb).toBe('DLET');
      expect(r.execDliBlocks[0].segmentName).toBeUndefined();
    });

    it('multi-line EXEC DLI accumulates correctly', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC DLI GN',
        '               USING PCB(1)',
        '               SEGMENT(ORDER)',
        '               INTO(ORDER-IO)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execDliBlocks).toHaveLength(1);
      expect(r.execDliBlocks[0].verb).toBe('GN');
      expect(r.execDliBlocks[0].segmentName).toBe('ORDER');
      expect(r.execDliBlocks[0].intoField).toBe('ORDER-IO');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: SET statement edge cases
  // -------------------------------------------------------------------------
  describe('SET statement edge cases', () => {
    it('SET multiple conditions TO TRUE', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SET COND-A COND-B COND-C TO TRUE.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].targets).toEqual(['COND-A', 'COND-B', 'COND-C']);
      expect(r.sets[0].form).toBe('to-true');
    });

    it('SET index DOWN BY identifier', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SET IDX-1 DOWN BY WS-DECREMENT.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].form).toBe('down-by');
      expect(r.sets[0].value).toBe('WS-DECREMENT');
    });

    it('SET index TO numeric value', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SET IDX-1 TO 5.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].form).toBe('to-value');
      expect(r.sets[0].value).toBe('5');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: INSPECT multi-line edge cases
  // -------------------------------------------------------------------------
  describe('INSPECT multi-line edge cases', () => {
    it('INSPECT CONVERTING on single line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           INSPECT WS-FIELD CONVERTING 'abc' TO 'ABC'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].form).toBe('converting');
      expect(r.inspects[0].inspectedField).toBe('WS-FIELD');
    });

    it('INSPECT TALLYING with multiple counters', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INSPECT WS-STRING TALLYING',
        "               WS-CNT-A FOR ALL 'A'",
        "               WS-CNT-B FOR ALL 'B'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].counters).toEqual(['WS-CNT-A', 'WS-CNT-B']);
    });

    it('INSPECT combined TALLYING and REPLACING', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INSPECT WS-DATA',
        "               TALLYING WS-COUNT FOR ALL 'X'",
        "               REPLACING ALL 'X' BY 'Y'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].form).toBe('tallying-replacing');
    });

    it('real paragraph header during INSPECT flushes accumulator', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           INSPECT WS-FIELD REPLACING ALL 'A' BY 'B'",
        '       NEXT-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // INSPECT should be flushed, NEXT-PARA should be detected
      expect(r.inspects).toHaveLength(1);
      expect(r.paragraphs.map((p) => p.name)).toContain('NEXT-PARA');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: DECLARATIVES edge cases
  // -------------------------------------------------------------------------
  describe('DECLARATIVES edge cases', () => {
    it('USE AFTER without STANDARD keyword (IBM extension)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '      DECLARATIVES.',
        '      FILE-ERR SECTION.',
        '          USE AFTER EXCEPTION ON MASTER-FILE.',
        '       FILE-ERR-PARA.',
        '           DISPLAY "ERROR".',
        '      END DECLARATIVES.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.declaratives).toHaveLength(1);
      expect(r.declaratives[0].target).toBe('MASTER-FILE');
    });

    it('USE AFTER on I-O mode (catch-all handler)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '      DECLARATIVES.',
        '      IO-ERR SECTION.',
        '          USE AFTER STANDARD ERROR ON I-O.',
        '       IO-ERR-PARA.',
        '           DISPLAY "I-O ERROR".',
        '      END DECLARATIVES.',
        '       MAIN-PARA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.declaratives).toHaveLength(1);
      expect(r.declaratives[0].target).toBe('I-O');
    });

    it('paragraphs after END DECLARATIVES are normal paragraphs', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '      DECLARATIVES.',
        '      ERR SECTION.',
        '          USE AFTER STANDARD ERROR ON INPUT.',
        '       ERR-PARA.',
        '           DISPLAY "E".',
        '      END DECLARATIVES.',
        '       MAIN-PARA.',
        '           PERFORM PROCESS-DATA.',
        '       PROCESS-DATA.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const paraNames = r.paragraphs.map((p) => p.name);
      expect(paraNames).toContain('ERR-PARA');
      expect(paraNames).toContain('MAIN-PARA');
      expect(paraNames).toContain('PROCESS-DATA');
      expect(r.performs).toHaveLength(1);
      expect(r.performs[0].target).toBe('PROCESS-DATA');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: COPY REPLACING edge cases
  // -------------------------------------------------------------------------
  describe('COPY REPLACING edge cases', () => {
    it('pseudotext replacement with empty target (deletion)', () => {
      const replacings = parseReplacingClause('==OLD-TEXT== BY ====');
      expect(replacings).toHaveLength(1);
      expect(replacings[0].from).toBe('OLD-TEXT');
      expect(replacings[0].to).toBe('');
      expect(replacings[0].isPseudotext).toBe(true);
    });
  });

  // -------------------------------------------------------------------------
  // Regression: Value clause edge cases
  // -------------------------------------------------------------------------
  describe('Value clause edge cases', () => {
    it('VALUE with hex literal', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        "       01 WS-HEX PIC X(4) VALUE X'F1F2F3F4'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const hex = r.dataItems.find((d) => d.name === 'WS-HEX');
      expect(hex).toBeDefined();
      expect(hex!.values).toBeDefined();
      expect(hex!.values![0]).toContain('F1F2F3F4');
    });

    it('VALUE with negative numeric', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-NEG PIC S9(4) VALUE -1.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.find((d) => d.name === 'WS-NEG')?.values).toEqual(['-1']);
    });

    it('VALUE with ALL literal', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        "       01 WS-STARS PIC X(80) VALUE ALL '*'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const stars = r.dataItems.find((d) => d.name === 'WS-STARS');
      expect(stars?.values).toBeDefined();
      expect(stars!.values![0]).toContain('*');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: OCCURS DEPENDING ON edge cases
  // -------------------------------------------------------------------------
  describe('OCCURS DEPENDING ON edge cases', () => {
    it('OCCURS with TO range', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-CNT PIC 9(4).',
        '       01 WS-TBL OCCURS 1 TO 50 DEPENDING ON WS-CNT.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const tbl = r.dataItems.find((d) => d.name === 'WS-TBL');
      expect(tbl?.occurs).toBe(1);
      expect(tbl?.dependingOn).toBe('WS-CNT');
    });

    it('OCCURS without DEPENDING ON (fixed-size)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-ARR OCCURS 10.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.find((d) => d.name === 'WS-ARR')?.occurs).toBe(10);
      expect(r.dataItems.find((d) => d.name === 'WS-ARR')?.dependingOn).toBeUndefined();
    });
  });

  // -------------------------------------------------------------------------
  // Regression: Dynamic CALL edge cases
  // -------------------------------------------------------------------------
  describe('Dynamic CALL edge cases', () => {
    it('dynamic CALL at end of line (no trailing space or period)', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CALL WS-PROGRAM',
        '               USING WS-DATA.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('WS-PROGRAM');
      expect(r.calls[0].isQuoted).toBe(false);
    });

    it('CANCEL at end of line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           CANCEL WS-OLD-PROG.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.cancels).toHaveLength(1);
      expect(r.cancels[0].target).toBe('WS-OLD-PROG');
      expect(r.cancels[0].isQuoted).toBe(false);
    });

    it('multiple CANCELs on same line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CANCEL 'PROG-A' CANCEL 'PROG-B'.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.cancels).toHaveLength(2);
      expect(r.cancels[0].target).toBe('PROG-A');
      expect(r.cancels[1].target).toBe('PROG-B');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: EXEC SQL edge cases
  // -------------------------------------------------------------------------
  describe('EXEC SQL edge cases', () => {
    it('EXEC SQL INCLUDE does not extract tables', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '           EXEC SQL INCLUDE SQLCA END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks).toHaveLength(1);
      expect(r.execSqlBlocks[0].includeMember).toBe('SQLCA');
      expect(r.execSqlBlocks[0].tables).toHaveLength(0);
    });

    it('EXEC SQL SELECT INTO host variable does not capture as table', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL',
        '               SELECT CUST_NAME INTO :WS-NAME',
        '               FROM CUSTOMER',
        '               WHERE CUST_ID = :WS-ID',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks).toHaveLength(1);
      // CUSTOMER should be a table, :WS-NAME should NOT
      expect(r.execSqlBlocks[0].tables).toContain('CUSTOMER');
      expect(r.execSqlBlocks[0].tables).not.toContain('WS-NAME');
    });

    it('EXEC SQL with host variables extracted', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL',
        '               UPDATE CUSTOMER SET BALANCE = :WS-AMT',
        '               WHERE CUST_ID = :WS-ID',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.execSqlBlocks[0].hostVariables).toContain('WS-AMT');
      expect(r.execSqlBlocks[0].hostVariables).toContain('WS-ID');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: INITIALIZE extraction
  // -------------------------------------------------------------------------
  describe('INITIALIZE extraction', () => {
    it('INITIALIZE extracts target field', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INITIALIZE WS-CUSTOMER-REC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.initializes).toHaveLength(1);
      expect(r.initializes[0].target).toBe('WS-CUSTOMER-REC');
      expect(r.initializes[0].caller).toBe('MAIN-PARA');
    });

    it('INITIALIZE multi-target extracts all targets', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INITIALIZE WS-CUSTOMER WS-ORDER WS-LINE-ITEM.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.initializes).toHaveLength(3);
      expect(r.initializes.map((i) => i.target)).toEqual([
        'WS-CUSTOMER',
        'WS-ORDER',
        'WS-LINE-ITEM',
      ]);
    });

    it('INITIALIZE with REPLACING clause does not capture keywords as targets', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INITIALIZE WS-RECORD REPLACING NUMERIC BY ZEROS.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.initializes).toHaveLength(1);
      expect(r.initializes[0].target).toBe('WS-RECORD');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: Nested program boundary tracking
  // -------------------------------------------------------------------------
  describe('Nested program boundary tracking', () => {
    it('sibling programs after END PROGRAM are correctly scoped', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        '           STOP RUN.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-A.',
        '      PROCEDURE DIVISION.',
        '       A-MAIN.',
        '           STOP RUN.',
        '       END PROGRAM INNER-A.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-B.',
        '      PROCEDURE DIVISION.',
        '       B-MAIN.',
        '           STOP RUN.',
        '       END PROGRAM INNER-B.',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programs).toHaveLength(3);
      expect(r.programs.map((p) => p.name).sort()).toEqual(['INNER-A', 'INNER-B', 'OUTER']);
      const innerA = r.programs.find((p) => p.name === 'INNER-A')!;
      const innerB = r.programs.find((p) => p.name === 'INNER-B')!;
      expect(innerA.endLine).toBe(11); // END PROGRAM INNER-A
      expect(innerB.startLine).toBe(13); // PROGRAM-ID. INNER-B
    });

    it('PROGRAM-ID without IDENTIFICATION DIVISION header detected', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        '           STOP RUN.',
        '       PROGRAM-ID. SIBLING.',
        '      PROCEDURE DIVISION.',
        '       SIB-MAIN.',
        '           STOP RUN.',
        '       END PROGRAM SIBLING.',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const names = r.programs.map((p) => p.name);
      expect(names).toContain('SIBLING');
      expect(names).toContain('OUTER');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: EXEC block EOF flush
  // -------------------------------------------------------------------------
  describe('EXEC block EOF flush', () => {
    it('unclosed EXEC SQL is flushed at EOF', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           EXEC SQL',
        '               SELECT * FROM CUSTOMER',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // Should still extract even without END-EXEC
      expect(r.execSqlBlocks).toHaveLength(1);
      expect(r.execSqlBlocks[0].tables).toContain('CUSTOMER');
    });
  });

  // -------------------------------------------------------------------------
  // Regression: Multi-PERFORM on same line
  // -------------------------------------------------------------------------
  describe('Multi-PERFORM on same line', () => {
    it('captures both PERFORMs in IF/ELSE on single line', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           IF WS-FLAG = 1 PERFORM PARA-A ELSE PERFORM PARA-B END-IF.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const targets = r.performs.map((p) => p.target).sort();
      expect(targets).toEqual(['PARA-A', 'PARA-B']);
    });
  });

  // -------------------------------------------------------------------------
  // Regression: Data item IS EXTERNAL / IS GLOBAL
  // -------------------------------------------------------------------------
  describe('Data item IS EXTERNAL / IS GLOBAL', () => {
    it('IS EXTERNAL does not pollute usage string', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-SHARED PIC X(10) USAGE DISPLAY IS EXTERNAL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const item = r.dataItems.find((d) => d.name === 'WS-SHARED');
      expect(item?.isExternal).toBe(true);
      // usage should NOT contain 'external' as a string suffix
      expect(item?.usage).toBe('DISPLAY');
    });
  });

  // -------------------------------------------------------------------------
  // Accumulator flush on division transitions
  // -------------------------------------------------------------------------
  describe('Accumulator flush on division transitions', () => {
    it('callAccum flushed when EXEC SQL interrupts multi-line CALL', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'SUBPROG'",
        '               USING WS-PARM',
        '           EXEC SQL',
        '               SELECT * FROM CUSTOMER',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // CALL should be extracted with USING parameters (flushed before EXEC SQL)
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('SUBPROG');
      expect(r.calls[0].parameters).toEqual(['WS-PARM']);
      // EXEC SQL should also be extracted
      expect(r.execSqlBlocks).toHaveLength(1);
      expect(r.execSqlBlocks[0].tables).toContain('CUSTOMER');
    });

    it('callAccum flushed when EXEC CICS interrupts multi-line CALL', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'SUBPROG'",
        '               USING WS-DATA',
        '           EXEC CICS',
        "               LINK PROGRAM('AUDITLOG')",
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-DATA']);
      expect(r.execCicsBlocks).toHaveLength(1);
    });

    it('callAccum flushed when EXEC DLI interrupts multi-line CALL', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'SUBPROG'",
        '               USING WS-KEY',
        '           EXEC DLI GU',
        '               USING PCB(1)',
        '               SEGMENT(CUSTOMER)',
        '           END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-KEY']);
      expect(r.execDliBlocks).toHaveLength(1);
      expect(r.execDliBlocks[0].verb).toBe('GU');
    });

    it('all accumulators flushed on division transition', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER-PGM.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'SUBPROG'",
        '               USING WS-DATA',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER-PGM.',
        '      PROCEDURE DIVISION.',
        '       INNER-MAIN.',
        '           STOP RUN.',
        '       END PROGRAM INNER-PGM.',
        '       END PROGRAM OUTER-PGM.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // CALL should be flushed before the new IDENTIFICATION DIVISION
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('SUBPROG');
      // Both programs should be detected
      expect(r.programs.map((p) => p.name).sort()).toEqual(['INNER-PGM', 'OUTER-PGM']);
    });
  });

  // -------------------------------------------------------------------------
  // Free-format COBOL handling
  // -------------------------------------------------------------------------
  describe('Free-format COBOL handling', () => {
    it('free-format source detected via >>SOURCE FREE', () => {
      const src = [
        '>>SOURCE FORMAT IS FREE',
        'IDENTIFICATION DIVISION.',
        'PROGRAM-ID. FREEPROG.',
        'PROCEDURE DIVISION.',
        'MAIN-PARA.',
        '    PERFORM PROCESS-DATA.',
        'PROCESS-DATA.',
        '    STOP RUN.',
      ].join('\n');
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('FREEPROG');
      expect(r.paragraphs).toHaveLength(2);
      expect(r.performs).toHaveLength(1);
    });

    it('free-format *> comments stripped but not inside quotes', () => {
      const src = [
        '>>SOURCE FREE',
        'IDENTIFICATION DIVISION.',
        'PROGRAM-ID. TESTPROG.',
        'DATA DIVISION.',
        'WORKING-STORAGE SECTION.',
        '01 WS-DATA PIC X(10). *> this is a comment',
      ].join('\n');
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.dataItems.find((d) => d.name === 'WS-DATA')).toBeDefined();
    });
  });

  // -------------------------------------------------------------------------
  // CANCEL extraction in CALL ON EXCEPTION block
  // -------------------------------------------------------------------------
  describe('CANCEL extraction in CALL ON EXCEPTION block', () => {
    it('CANCEL inside CALL END-CALL block is extracted', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'MAINPROG'",
        '               USING WS-DATA',
        '               ON EXCEPTION',
        "                   CANCEL 'MAINPROG'",
        "                   CALL 'BACKUP-PGM'",
        '           END-CALL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // Both CALLs should be captured
      expect(r.calls).toHaveLength(2);
      expect(r.calls.map((c) => c.target).sort()).toEqual(['BACKUP-PGM', 'MAINPROG']);
      // CANCEL should be captured from within the CALL block
      expect(r.cancels).toHaveLength(1);
      expect(r.cancels[0].target).toBe('MAINPROG');
    });
  });

  // -------------------------------------------------------------------------
  // SORT INPUT PROCEDURE THRU range
  // -------------------------------------------------------------------------
  describe('SORT INPUT PROCEDURE THRU range', () => {
    it('captures both start and thru target', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SORT SORT-FILE ON ASCENDING KEY SORT-KEY',
        '               INPUT PROCEDURE IS BUILD-INPUT THRU BUILD-END',
        '               OUTPUT PROCEDURE IS WRITE-OUTPUT.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      // INPUT PROCEDURE should produce a perform with thruTarget
      const inputProc = r.performs.find((p) => p.target === 'BUILD-INPUT');
      expect(inputProc).toBeDefined();
      expect(inputProc!.thruTarget).toBe('BUILD-END');
      // OUTPUT PROCEDURE should be captured too
      expect(r.performs.find((p) => p.target === 'WRITE-OUTPUT')).toBeDefined();
    });
  });

  // -------------------------------------------------------------------------
  // Shared verb constant coverage
  // -------------------------------------------------------------------------
  describe('Shared verb constant coverage', () => {
    it('COBOL_STATEMENT_VERBS flush trigger works for all major verbs', () => {
      // Test that each verb in the shared constant terminates callAccum
      const verbs = [
        'PERFORM NEXT-PARA.',
        'MOVE WS-A TO WS-B.',
        'DISPLAY "HELLO".',
        'GO TO EXIT-PARA.',
        'INSPECT WS-X REPLACING ALL SPACES BY ZEROS.',
        'SET WS-FLAG TO TRUE.',
        'INITIALIZE WS-REC.',
        'CANCEL WS-OLD.',
      ];
      for (const verb of verbs) {
        const src = cobol(
          '      IDENTIFICATION DIVISION.',
          '       PROGRAM-ID. TESTPROG.',
          '      PROCEDURE DIVISION.',
          '       MAIN-PARA.',
          "           CALL 'PGM'",
          '               USING WS-PARM',
          `           ${verb}`,
        );
        const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
        expect(r.calls.length).toBe(1);
        expect(r.calls[0].parameters).toEqual(['WS-PARM']);
      }
    });
  });

  // -------------------------------------------------------------------------
  // EXEC SQL INCLUDE edge cases
  // -------------------------------------------------------------------------
  describe('EXEC SQL INCLUDE edge cases', () => {
    it('multiple EXEC SQL INCLUDEs extracted', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '           EXEC SQL INCLUDE SQLCA END-EXEC.',
        '           EXEC SQL INCLUDE SQLDA END-EXEC.',
        '           EXEC SQL INCLUDE CUSTDCL END-EXEC.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      const includes = r.execSqlBlocks.filter((b) => b.includeMember);
      expect(includes).toHaveLength(3);
      expect(includes.map((i) => i.includeMember).sort()).toEqual(['CUSTDCL', 'SQLCA', 'SQLDA']);
    });
  });

  // -------------------------------------------------------------------------
  // Complete COBOL program integration
  // -------------------------------------------------------------------------
  describe('Complete COBOL program integration', () => {
    it('extracts all construct types from a realistic program', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. FULLTEST.',
        '       AUTHOR. TEST AUTHOR.',
        '      ENVIRONMENT DIVISION.',
        '      INPUT-OUTPUT SECTION.',
        '      FILE-CONTROL.',
        "          SELECT CUST-FILE ASSIGN TO 'CUSTFILE'",
        '              ORGANIZATION IS INDEXED',
        '              ACCESS IS DYNAMIC',
        '              RECORD KEY IS CUST-ID.',
        '      DATA DIVISION.',
        '      WORKING-STORAGE SECTION.',
        '       01 WS-COUNT PIC 9(4) VALUE 0.',
        '       01 WS-TABLE OCCURS 10 DEPENDING ON WS-COUNT.',
        '       01 WS-FLAG PIC 9 VALUE 0.',
        '           88 END-OF-FILE VALUE 1.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           PERFORM PROCESS-DATA',
        '           SET END-OF-FILE TO TRUE',
        "           CALL 'SUBPROG' USING WS-COUNT.",
        '       PROCESS-DATA.',
        "           INSPECT WS-FLAG REPLACING ALL '0' BY '1'.",
        '           INITIALIZE WS-TABLE.',
        '           STOP RUN.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.programName).toBe('FULLTEST');
      expect(r.programMetadata.author).toBe('TEST AUTHOR');
      expect(r.fileDeclarations).toHaveLength(1);
      expect(r.fileDeclarations[0].organization).toBe('INDEXED');
      expect(r.dataItems.find((d) => d.name === 'WS-COUNT')?.values).toEqual(['0']);
      expect(r.dataItems.find((d) => d.name === 'WS-TABLE')?.dependingOn).toBe('WS-COUNT');
      expect(r.paragraphs).toHaveLength(2);
      expect(r.performs).toHaveLength(1);
      expect(r.sets).toHaveLength(1);
      expect(r.sets[0].form).toBe('to-true');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-COUNT']);
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].form).toBe('replacing');
      expect(r.initializes).toHaveLength(1);
    });
  });

  // -------------------------------------------------------------------------
  // Accumulator flush at END PROGRAM boundary
  // -------------------------------------------------------------------------
  describe('Accumulator flush at END PROGRAM boundary', () => {
    it('multi-line CALL flushed at END PROGRAM', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        "           CALL 'SUBPROG'",
        '               USING WS-DATA',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('SUBPROG');
      expect(r.calls[0].parameters).toEqual(['WS-DATA']);
    });

    it('multi-line CALL flushed at END PROGRAM in nested programs', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        '           STOP RUN.',
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. INNER.',
        '      PROCEDURE DIVISION.',
        '       INNER-MAIN.',
        "           CALL 'INNERSUB'",
        '               USING WS-INNER-DATA',
        '       END PROGRAM INNER.',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('INNERSUB');
      expect(r.calls[0].parameters).toEqual(['WS-INNER-DATA']);
      expect(r.programs).toHaveLength(2);
    });
  });

  // -------------------------------------------------------------------------
  // Accumulator flush at PROGRAM-ID sibling boundary
  // -------------------------------------------------------------------------
  describe('Accumulator flush at PROGRAM-ID sibling boundary', () => {
    it('multi-line CALL flushed when sibling PROGRAM-ID appears without ID DIVISION', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. OUTER.',
        '      PROCEDURE DIVISION.',
        '       OUTER-MAIN.',
        "           CALL 'OUTERSUB'",
        '               USING WS-OUTER',
        '       PROGRAM-ID. SIBLING.',
        '      PROCEDURE DIVISION.',
        '       SIB-MAIN.',
        '           STOP RUN.',
        '       END PROGRAM SIBLING.',
        '       END PROGRAM OUTER.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('OUTERSUB');
      expect(r.calls[0].parameters).toEqual(['WS-OUTER']);
      const names = r.programs.map((p) => p.name);
      expect(names).toContain('SIBLING');
    });
  });

  // -------------------------------------------------------------------------
  // Accumulator flush on arithmetic verb boundaries
  // -------------------------------------------------------------------------
  describe('Accumulator flush on arithmetic verb boundaries', () => {
    it('COMPUTE terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-INPUT',
        '           COMPUTE WS-TOTAL = WS-A + WS-B.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-INPUT']);
    });

    it('ADD terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-AMT',
        '           ADD WS-AMT TO WS-TOTAL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-AMT']);
    });

    it('SUBTRACT terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-VAL',
        '           SUBTRACT WS-DISCOUNT FROM WS-TOTAL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-VAL']);
    });

    it('MULTIPLY terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-QTY',
        '           MULTIPLY WS-PRICE BY WS-QTY.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-QTY']);
    });

    it('DIVIDE terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-TOTAL',
        '           DIVIDE WS-TOTAL BY WS-COUNT GIVING WS-AVG.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-TOTAL']);
    });

    it('STRING terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-NAME',
        '           STRING WS-FIRST DELIMITED BY SIZE INTO WS-FULL.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-NAME']);
    });

    it('UNSTRING terminates multi-line CALL accumulation', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM'",
        '               USING WS-LINE',
        "           UNSTRING WS-LINE DELIMITED BY ',' INTO WS-A WS-B.",
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].parameters).toEqual(['WS-LINE']);
    });
  });

  // -------------------------------------------------------------------------
  // Arithmetic verbs not captured as false USING parameters
  // -------------------------------------------------------------------------
  describe('Arithmetic verbs not captured as false USING parameters', () => {
    it('COMPUTE after CALL USING does not pollute parameters', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        "           CALL 'PGM' USING WS-INPUT.",
        '           COMPUTE WS-RESULT = WS-A * WS-B.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.calls).toHaveLength(1);
      // Only WS-INPUT should be a parameter, not WS-RESULT/WS-A/WS-B
      expect(r.calls[0].parameters).toEqual(['WS-INPUT']);
    });
  });

  // -------------------------------------------------------------------------
  // ReDoS audit slice
  // -------------------------------------------------------------------------
  describe('ReDoS audit slice', () => {
    it('handles a bounded large CALL USING parameter list without false extra symbols', () => {
      const parameters = Array.from({ length: 2000 }, (_, i) => `WS-PARM-${i}`).join(' ');
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        `           CALL 'PGM' USING ${parameters}.`,
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');

      expect(r.calls).toHaveLength(1);
      expect(r.calls[0].target).toBe('PGM');
      expect(r.calls[0].parameters).toHaveLength(2000);
      expect(r.calls[0].parameters![0]).toBe('WS-PARM-0');
      expect(r.calls[0].parameters![1999]).toBe('WS-PARM-1999');
    });

    it('does not treat a bounded large failed SET TO TRUE candidate as condition names', () => {
      const targets = Array.from({ length: 2000 }, (_, i) => `WS-FLAG-${i}`).join(' ');
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        `           SET ${targets} TO-FALSE.`,
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');

      expect(r.sets).toHaveLength(0);
      expect(r.paragraphs).toEqual([{ name: 'MAIN-PARA', line: 4 }]);
    });
  });

  // -------------------------------------------------------------------------
  // SORT accumulator flushed at program boundaries
  // -------------------------------------------------------------------------
  describe('SORT accumulator flushed at program boundaries', () => {
    it('multi-line SORT flushed at END PROGRAM', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           SORT SORT-FILE',
        '               USING INPUT-FILE',
        '       END PROGRAM TESTPROG.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.sorts).toHaveLength(1);
      expect(r.sorts[0].sortFile).toBe('SORT-FILE');
      expect(r.sorts[0].usingFiles).toEqual(['INPUT-FILE']);
    });
  });

  // -------------------------------------------------------------------------
  // INSPECT accumulator flushed at program boundaries
  // -------------------------------------------------------------------------
  describe('INSPECT accumulator flushed at program boundaries', () => {
    it('multi-line INSPECT flushed at END PROGRAM', () => {
      const src = cobol(
        '      IDENTIFICATION DIVISION.',
        '       PROGRAM-ID. TESTPROG.',
        '      PROCEDURE DIVISION.',
        '       MAIN-PARA.',
        '           INSPECT WS-DATA',
        "               REPLACING ALL 'X' BY 'Y'",
        '       END PROGRAM TESTPROG.',
      );
      const r = extractCobolSymbolsWithRegex(src, 'test.cbl');
      expect(r.inspects).toHaveLength(1);
      expect(r.inspects[0].inspectedField).toBe('WS-DATA');
      expect(r.inspects[0].form).toBe('replacing');
    });
  });
});
