import { describe, it, expect, beforeEach } from 'vitest';
import { TypeScriptFieldExtractor } from '../../src/core/ingestion/field-extractors/typescript.js';
import { createFieldExtractor } from '../../src/core/ingestion/field-extractors/generic.js';
import { typescriptConfig } from '../../src/core/ingestion/field-extractors/configs/typescript-javascript.js';
import { pythonConfig } from '../../src/core/ingestion/field-extractors/configs/python.js';
import { goConfig } from '../../src/core/ingestion/field-extractors/configs/go.js';
import { cppConfig } from '../../src/core/ingestion/field-extractors/configs/c-cpp.js';
import { rubyConfig } from '../../src/core/ingestion/field-extractors/configs/ruby.js';
import { dartConfig } from '../../src/core/ingestion/field-extractors/configs/dart.js';
import { kotlinConfig } from '../../src/core/ingestion/field-extractors/configs/jvm.js';
import { swiftConfig } from '../../src/core/ingestion/field-extractors/configs/swift.js';
import type { FieldExtractorContext } from '../../src/core/ingestion/field-types.js';
import type { TypeEnvironment } from '../../src/core/ingestion/type-env.js';
import { createSemanticModel } from '../../src/core/ingestion/model/semantic-model.js';
import Parser from 'tree-sitter';
import TypeScript from 'tree-sitter-typescript';
import Python from 'tree-sitter-python';
import Go from 'tree-sitter-go';
import Cpp from 'tree-sitter-cpp';
import Ruby from 'tree-sitter-ruby';
import CSharp from 'tree-sitter-c-sharp';
import Dart from 'tree-sitter-dart';

let Kotlin: unknown;
try {
  Kotlin = require('tree-sitter-kotlin');
} catch {
  // Kotlin grammar may not be installed
}

let Swift: unknown;
try {
  Swift = require('tree-sitter-swift');
} catch {
  // Swift grammar is an optional dependency; may not be installed
}
import { csharpConfig as csharpFieldConfig } from '../../src/core/ingestion/field-extractors/configs/csharp.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';

const parser = new Parser();

const parse = (code: string) => {
  parser.setLanguage(TypeScript.typescript);
  return parser.parse(code);
};

// Mock context for tests. symbolTable comes from createSemanticModel().symbols
// (the facade) rather than createSymbolTable() (the raw leaf) — this mirrors
// production, where FieldExtractorContext always receives the SemanticModel-
// wrapped facade so any .add() write dispatches through the owner-scoped
// registries. No current field extractor calls symbolTable.add(), but
// matching the production shape prevents silent drift if a future extractor
// starts registering dynamically-discovered properties.
const createMockContext = (): FieldExtractorContext => ({
  typeEnv: {
    lookup: () => undefined,
    constructorBindings: [],
    fileScope: () => new Map(),
    allScopes: () => new Map(),
    constructorTypeMap: new Map(),
  } as TypeEnvironment,
  symbolTable: createSemanticModel().symbols,
  filePath: 'test.ts',
  language: SupportedLanguages.TypeScript,
});

describe('TypeScriptFieldExtractor', () => {
  let extractor: TypeScriptFieldExtractor;
  let mockContext: FieldExtractorContext;

  beforeEach(() => {
    extractor = new TypeScriptFieldExtractor();
    mockContext = createMockContext();
  });

  describe('isTypeDeclaration', () => {
    it('recognizes class_declaration', () => {
      const tree = parse('class User {}');
      const classNode = tree.rootNode.child(0);
      expect(classNode).toBeDefined();
      expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
    });

    it('recognizes interface_declaration', () => {
      const tree = parse('interface IUser {}');
      const interfaceNode = tree.rootNode.child(0);
      expect(interfaceNode).toBeDefined();
      expect(extractor.isTypeDeclaration(interfaceNode!)).toBe(true);
    });

    it('recognizes abstract_class_declaration', () => {
      const tree = parse('abstract class BaseService {}');
      const abstractNode = tree.rootNode.child(0);
      expect(abstractNode).toBeDefined();
      expect(extractor.isTypeDeclaration(abstractNode!)).toBe(true);
    });

    it('rejects function_declaration', () => {
      const tree = parse('function getUser() {}');
      const functionNode = tree.rootNode.child(0);
      expect(functionNode).toBeDefined();
      expect(extractor.isTypeDeclaration(functionNode!)).toBe(false);
    });

    it('rejects variable declaration', () => {
      const tree = parse('const user = {};');
      const variableNode = tree.rootNode.child(0);
      expect(variableNode).toBeDefined();
      expect(extractor.isTypeDeclaration(variableNode!)).toBe(false);
    });
  });

  describe('extract', () => {
    it('extracts single field with type', () => {
      const tree = parse(`
        class User {
          name: string;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.ownerFqn).toBe('User');
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('name');
      expect(result!.fields[0].type).toBe('string');
      expect(result!.fields[0].visibility).toBe('public');
    });

    it('extracts private field', () => {
      const tree = parse(`
        class User {
          private password: string;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('password');
      expect(result!.fields[0].type).toBe('string');
      expect(result!.fields[0].visibility).toBe('private');
    });

    it('extracts static readonly field', () => {
      const tree = parse(`
        class Config {
          static readonly VERSION: string = '1.0';
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('VERSION');
      expect(result!.fields[0].type).toBe('string');
      expect(result!.fields[0].isStatic).toBe(true);
      expect(result!.fields[0].isReadonly).toBe(true);
      expect(result!.fields[0].visibility).toBe('public');
    });

    it('extracts optional field (?:)', () => {
      const tree = parse(`
        interface User {
          email?: string;
        }
      `);
      const interfaceNode = tree.rootNode.child(0);
      const result = extractor.extract(interfaceNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('email');
      // Note: optional fields may have type modified to include undefined
      expect(result!.fields[0].type).toContain('string');
    });

    it('extracts multiple fields with different visibilities', () => {
      const tree = parse(`
        class User {
          public id: number;
          private secretKey: string;
          protected createdAt: Date;
          name: string;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(4);

      const fields = result!.fields;

      const idField = fields.find((f) => f.name === 'id');
      expect(idField).toBeDefined();
      expect(idField!.visibility).toBe('public');
      expect(idField!.type).toBe('number');

      const secretKeyField = fields.find((f) => f.name === 'secretKey');
      expect(secretKeyField).toBeDefined();
      expect(secretKeyField!.visibility).toBe('private');

      const createdAtField = fields.find((f) => f.name === 'createdAt');
      expect(createdAtField).toBeDefined();
      expect(createdAtField!.visibility).toBe('protected');

      const nameField = fields.find((f) => f.name === 'name');
      expect(nameField).toBeDefined();
      expect(nameField!.visibility).toBe('public'); // default
    });

    it('handles field without type annotation', () => {
      const tree = parse(`
        class User {
          name;
          age;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(2);

      const nameField = result!.fields.find((f) => f.name === 'name');
      expect(nameField).toBeDefined();
      expect(nameField!.type).toBeNull();

      const ageField = result!.fields.find((f) => f.name === 'age');
      expect(ageField).toBeDefined();
      expect(ageField!.type).toBeNull();
    });

    it('extracts complex generic types (Map<string, User>, Array<number>)', () => {
      const tree = parse(`
        class Repository {
          users: Map<string, User>;
          ids: Array<number>;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(2);

      const usersField = result!.fields.find((f) => f.name === 'users');
      expect(usersField).toBeDefined();
      expect(usersField!.type).toBe('Map<string, User>');

      const idsField = result!.fields.find((f) => f.name === 'ids');
      expect(idsField).toBeDefined();
      expect(idsField!.type).toBe('Array<number>');
    });

    it('extracts nested types', () => {
      const tree = parse(`
        class Container {
          data: OuterType<InnerType<string>>;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('data');
      expect(result!.fields[0].type).toBe('OuterType<InnerType<string>>');
    });

    it('extracts fields from interface', () => {
      const tree = parse(`
        interface UserDTO {
          id: number;
          name: string;
          email?: string;
        }
      `);
      const interfaceNode = tree.rootNode.child(0);
      const result = extractor.extract(interfaceNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.ownerFqn).toBe('UserDTO');
      expect(result!.fields).toHaveLength(3);

      const idField = result!.fields.find((f) => f.name === 'id');
      expect(idField).toBeDefined();
      expect(idField!.type).toBe('number');

      const nameField = result!.fields.find((f) => f.name === 'name');
      expect(nameField).toBeDefined();
      expect(nameField!.type).toBe('string');

      const emailField = result!.fields.find((f) => f.name === 'email');
      expect(emailField).toBeDefined();
    });

    it('extracts fields from abstract class', () => {
      const tree = parse(`
        abstract class BaseEntity {
          protected id: number;
          createdAt: Date;
        }
      `);
      const abstractNode = tree.rootNode.child(0);
      const result = extractor.extract(abstractNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.ownerFqn).toBe('BaseEntity');
      expect(result!.fields).toHaveLength(2);

      const idField = result!.fields.find((f) => f.name === 'id');
      expect(idField).toBeDefined();
      expect(idField!.visibility).toBe('protected');

      const createdAtField = result!.fields.find((f) => f.name === 'createdAt');
      expect(createdAtField).toBeDefined();
      expect(createdAtField!.visibility).toBe('public');
    });

    it('extracts array types', () => {
      const tree = parse(`
        class UserService {
          users: User[];
          ids: number[];
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(2);

      const usersField = result!.fields.find((f) => f.name === 'users');
      expect(usersField).toBeDefined();
      expect(usersField!.type).toBe('User[]');

      const idsField = result!.fields.find((f) => f.name === 'ids');
      expect(idsField).toBeDefined();
      expect(idsField!.type).toBe('number[]');
    });

    it('extracts union types', () => {
      const tree = parse(`
        class Field {
          value: string | number | null;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('value');
      expect(result!.fields[0].type).toBe('string | number | null');
    });

    it('returns null for non-type declaration nodes', () => {
      const tree = parse('function getUser() {}');
      const functionNode = tree.rootNode.child(0);
      const result = extractor.extract(functionNode!, mockContext);
      expect(result).toBeNull();
    });

    it('extracts fields from type alias with object type', () => {
      const tree = parse(`
        type UserDTO = {
          id: number;
          name: string;
        }
      `);
      const typeAliasNode = tree.rootNode.child(0);
      const result = extractor.extract(typeAliasNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.ownerFqn).toBe('UserDTO');
      expect(result!.fields).toHaveLength(2);

      const idField = result!.fields.find((f) => f.name === 'id');
      expect(idField).toBeDefined();
      expect(idField!.type).toBe('number');

      const nameField = result!.fields.find((f) => f.name === 'name');
      expect(nameField).toBeDefined();
      expect(nameField!.type).toBe('string');
    });

    it('includes source file path in field info', () => {
      const tree = parse(`
        class User {
          name: string;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].sourceFile).toBe('test.ts');
    });

    it('includes line number in field info', () => {
      const tree = parse(`
        class User {
          name: string;
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].line).toBeGreaterThan(0);
    });

    it('detects nested interface declarations in methods', () => {
      const tree = parse(`
        class Container {
          data: string;
          
          process() {
            interface LocalInterface {
              value: number;
            }
          }
        }
      `);
      const classNode = tree.rootNode.child(0);
      const result = extractor.extract(classNode!, mockContext);

      expect(result).not.toBeNull();
      expect(result!.ownerFqn).toBe('Container');
      // Note: Nested types within method bodies are detected
      expect(result!.nestedTypes).toContain('LocalInterface');
      // Should only extract fields from the outer class
      expect(result!.fields).toHaveLength(1);
      expect(result!.fields[0].name).toBe('data');
    });
  });
});

// ---------------------------------------------------------------------------
// Generic factory tests — TypeScript config
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — TypeScript config', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(typescriptConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.TypeScript;

  it('extracts public and private fields from a class', () => {
    parser.setLanguage(TypeScript.typescript);
    const tree = parser.parse(`
      class User {
        public name: string;
        private age: number;
      }
    `);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();
    expect(extractor.isTypeDeclaration(classNode!)).toBe(true);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('User');
    expect(result!.fields).toHaveLength(2);

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.visibility).toBe('public');
    expect(nameField!.type).toBe('string');

    const ageField = result!.fields.find((f) => f.name === 'age');
    expect(ageField).toBeDefined();
    expect(ageField!.visibility).toBe('private');
    expect(ageField!.type).toBe('number');
  });

  it('uses body-discovery fallback when body type does not match config', () => {
    parser.setLanguage(TypeScript.typescript);
    // interface_declaration has an interface_body — which IS in the config bodyNodeTypes
    // but type_alias_declaration has an object_type body which is also in bodyNodeTypes
    const tree = parser.parse(`
      interface Settings {
        theme: string;
        debug: boolean;
      }
    `);
    const ifaceNode = tree.rootNode.child(0);
    expect(ifaceNode).toBeDefined();
    expect(extractor.isTypeDeclaration(ifaceNode!)).toBe(true);

    const result = extractor.extract(ifaceNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('Settings');
    expect(result!.fields).toHaveLength(2);
    expect(result!.fields.map((f) => f.name)).toContain('theme');
    expect(result!.fields.map((f) => f.name)).toContain('debug');
  });

  it('returns null for non-type-declaration nodes', () => {
    parser.setLanguage(TypeScript.typescript);
    const tree = parser.parse('function hello() {}');
    const fnNode = tree.rootNode.child(0);
    expect(extractor.isTypeDeclaration(fnNode!)).toBe(false);
    expect(extractor.extract(fnNode!, mockContext)).toBeNull();
  });

  it('extracts static and readonly modifiers', () => {
    parser.setLanguage(TypeScript.typescript);
    const tree = parser.parse(`
      class Config {
        static readonly MAX: number;
        private count: number;
      }
    `);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(2);

    const maxField = result!.fields.find((f) => f.name === 'MAX');
    expect(maxField).toBeDefined();
    expect(maxField!.isStatic).toBe(true);
    expect(maxField!.isReadonly).toBe(true);

    const countField = result!.fields.find((f) => f.name === 'count');
    expect(countField).toBeDefined();
    expect(countField!.isStatic).toBe(false);
    expect(countField!.isReadonly).toBe(false);
  });

  it('does not false-positive visibility on fields named after visibility keywords', () => {
    parser.setLanguage(TypeScript.typescript);
    // A class field literally named 'private' with no accessibility modifier —
    // findVisibility must not confuse the name with a keyword
    const tree = parser.parse(`
      class Flags {
        private: boolean;
      }
    `);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(1);
    expect(result!.fields[0].name).toBe('private');
    // Default visibility is public — the field NAME 'private' must not be treated as a keyword
    expect(result!.fields[0].visibility).toBe('public');
  });
});

// ---------------------------------------------------------------------------
// Python config
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — Python', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(pythonConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Python;
  mockContext.filePath = 'test.py';

  it('extracts annotated class fields', () => {
    parser.setLanguage(Python);
    const tree = parser.parse(`class User:
    name: str
    email: str
`);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();
    expect(extractor.isTypeDeclaration(classNode!)).toBe(true);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('User');
    expect(result!.fields).toHaveLength(2);

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.type).toBe('str');
    expect(nameField!.visibility).toBe('public');

    const emailField = result!.fields.find((f) => f.name === 'email');
    expect(emailField).toBeDefined();
    expect(emailField!.type).toBe('str');
  });

  it('detects underscore-based visibility: _protected and __private', () => {
    parser.setLanguage(Python);
    const tree = parser.parse(`class Settings:
    name: str
    _internal: int
    __secret: str
`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(3);

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.visibility).toBe('public');

    const internalField = result!.fields.find((f) => f.name === '_internal');
    expect(internalField).toBeDefined();
    expect(internalField!.visibility).toBe('protected');

    const secretField = result!.fields.find((f) => f.name === '__secret');
    expect(secretField).toBeDefined();
    expect(secretField!.visibility).toBe('private');
  });

  it('does not mark dunder attributes as private', () => {
    parser.setLanguage(Python);
    // __slots__ starts with __ but also ends with __ so the private check
    // (startsWith('__') && !endsWith('__')) is skipped.
    // However it still starts with _ so the config classifies it as protected.
    const tree = parser.parse(`class Meta:
    __slots__: list
`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(1);
    // __slots__ ends with __ → not private, but still starts with _ → protected
    expect(result!.fields[0].visibility).toBe('protected');
  });

  it('reports isStatic and isReadonly as false', () => {
    parser.setLanguage(Python);
    const tree = parser.parse(`class A:
    x: int
`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields[0].isStatic).toBe(false);
    expect(result!.fields[0].isReadonly).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Go config
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — Go', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(goConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Go;
  mockContext.filePath = 'test.go';

  // Helper: Go AST nests name under type_spec, not type_declaration.
  // The generic factory's extract() requires a node with a 'name' field,
  // so we walk to type_spec which holds the name.
  function findTypeSpec(src: string) {
    parser.setLanguage(Go);
    const tree = parser.parse(src);
    const typeDecl = tree.rootNode.child(0)!;
    return { typeDecl, typeSpec: typeDecl.namedChild(0)! };
  }

  it('recognizes type_declaration via isTypeDeclaration', () => {
    const { typeDecl } = findTypeSpec(`type User struct {\n\tName string\n}`);
    expect(typeDecl.type).toBe('type_declaration');
    expect(extractor.isTypeDeclaration(typeDecl)).toBe(true);
  });

  it('rejects non-type-declaration nodes', () => {
    parser.setLanguage(Go);
    const tree = parser.parse(`func main() {}`);
    const fnNode = tree.rootNode.child(0);
    expect(extractor.isTypeDeclaration(fnNode!)).toBe(false);
    expect(extractor.extract(fnNode!, mockContext)).toBeNull();
  });

  it('detects uppercase-based visibility via extractVisibility on field nodes', () => {
    const { typeDecl } = findTypeSpec(
      `type Config struct {\n\tHost string\n\tport int\n\tTimeout int\n}`,
    );
    // Navigate to field_declaration nodes inside the struct
    // type_declaration > type_spec > struct_type > field_declaration_list > field_declaration
    const typeSpec = typeDecl.namedChild(0)!;
    const structType = typeSpec.namedChild(1)!;
    const fieldList = structType.namedChild(0)!;

    expect(fieldList.type).toBe('field_declaration_list');

    const hostNode = fieldList.namedChild(0)!;
    const portNode = fieldList.namedChild(1)!;
    const timeoutNode = fieldList.namedChild(2)!;

    expect(goConfig.extractName(hostNode)).toBe('Host');
    expect(goConfig.extractVisibility(hostNode)).toBe('public');

    expect(goConfig.extractName(portNode)).toBe('port');
    expect(goConfig.extractVisibility(portNode)).toBe('package');

    expect(goConfig.extractName(timeoutNode)).toBe('Timeout');
    expect(goConfig.extractVisibility(timeoutNode)).toBe('public');
  });

  it('extracts field types', () => {
    const { typeDecl } = findTypeSpec(`type Point struct {\n\tX float64\n\tY float64\n}`);
    const typeSpec = typeDecl.namedChild(0)!;
    const structType = typeSpec.namedChild(1)!;
    const fieldList = structType.namedChild(0)!;

    const xNode = fieldList.namedChild(0)!;
    expect(goConfig.extractName(xNode)).toBe('X');
    expect(goConfig.extractType(xNode)).toBe('float64');
  });

  it('extracts every name from Go multi-name struct fields', () => {
    const { typeDecl } = findTypeSpec(`type Point struct {\n\tX, y int\n}`);

    const result = extractor.extract(typeDecl, mockContext);
    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(2);

    const xField = result!.fields.find((field) => field.name === 'X');
    expect(xField).toBeDefined();
    expect(xField!.type).toBe('int');
    expect(xField!.visibility).toBe('public');

    const yField = result!.fields.find((field) => field.name === 'y');
    expect(yField).toBeDefined();
    expect(yField!.type).toBe('int');
    expect(yField!.visibility).toBe('package');
  });

  it('extracts Go multi-name struct fields when called with the runtime struct_type owner', () => {
    const { typeDecl } = findTypeSpec(`type Point struct {\n\tX, y int\n}`);
    const typeSpec = typeDecl.namedChild(0)!;
    const structType = typeSpec.namedChild(1)!;

    expect(structType.type).toBe('struct_type');
    expect(extractor.isTypeDeclaration(structType)).toBe(true);

    const result = extractor.extract(structType, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('Point');
    expect(result!.fields).toHaveLength(2);

    const xField = result!.fields.find((field) => field.name === 'X');
    expect(xField).toBeDefined();
    expect(xField!.type).toBe('int');
    expect(xField!.visibility).toBe('public');

    const yField = result!.fields.find((field) => field.name === 'y');
    expect(yField).toBeDefined();
    expect(yField!.type).toBe('int');
    expect(yField!.visibility).toBe('package');
  });

  it('reports isStatic and isReadonly as false for all fields', () => {
    const { typeDecl } = findTypeSpec(`type S struct {\n\tX int\n}`);
    const typeSpec = typeDecl.namedChild(0)!;
    const structType = typeSpec.namedChild(1)!;
    const fieldList = structType.namedChild(0)!;
    const fieldNode = fieldList.namedChild(0)!;

    expect(goConfig.isStatic(fieldNode)).toBe(false);
    expect(goConfig.isReadonly(fieldNode)).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// C++ config
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — C++', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(cppConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.CPlusPlus;
  mockContext.filePath = 'test.cpp';

  it('extracts fields from a class with access specifiers', () => {
    parser.setLanguage(Cpp);
    const tree = parser.parse(`class User {
public:
  int id;
  std::string name;
private:
  std::string password;
};`);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();
    expect(extractor.isTypeDeclaration(classNode!)).toBe(true);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('User');
    expect(result!.fields).toHaveLength(3);

    const idField = result!.fields.find((f) => f.name === 'id');
    expect(idField).toBeDefined();
    expect(idField!.visibility).toBe('public');
    expect(idField!.type).toBe('int');

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.visibility).toBe('public');

    const pwField = result!.fields.find((f) => f.name === 'password');
    expect(pwField).toBeDefined();
    expect(pwField!.visibility).toBe('private');
  });

  it('uses backward-sibling walk to find access specifier', () => {
    parser.setLanguage(Cpp);
    // protected section followed by more fields
    const tree = parser.parse(`class Base {
protected:
  int x;
  int y;
public:
  int z;
};`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(3);

    const xField = result!.fields.find((f) => f.name === 'x');
    expect(xField).toBeDefined();
    expect(xField!.visibility).toBe('protected');

    const yField = result!.fields.find((f) => f.name === 'y');
    expect(yField).toBeDefined();
    expect(yField!.visibility).toBe('protected');

    const zField = result!.fields.find((f) => f.name === 'z');
    expect(zField).toBeDefined();
    expect(zField!.visibility).toBe('public');
  });

  it('defaults to private for class without access specifiers', () => {
    parser.setLanguage(Cpp);
    const tree = parser.parse(`class Secret {
  int value;
};`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(1);
    expect(result!.fields[0].name).toBe('value');
    expect(result!.fields[0].visibility).toBe('private');
  });

  it('defaults to public for struct without access specifiers', () => {
    parser.setLanguage(Cpp);
    const tree = parser.parse(`struct Point {
  double x;
  double y;
};`);
    const structNode = tree.rootNode.child(0);
    expect(extractor.isTypeDeclaration(structNode!)).toBe(true);

    const result = extractor.extract(structNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(2);
    expect(result!.fields[0].visibility).toBe('public');
    expect(result!.fields[1].visibility).toBe('public');
  });

  it('detects static and const fields', () => {
    parser.setLanguage(Cpp);
    const tree = parser.parse(`class Config {
public:
  static int count;
  const int MAX_SIZE;
};`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(2);

    const countField = result!.fields.find((f) => f.name === 'count');
    expect(countField).toBeDefined();
    expect(countField!.isStatic).toBe(true);

    const maxField = result!.fields.find((f) => f.name === 'MAX_SIZE');
    expect(maxField).toBeDefined();
    expect(maxField!.isReadonly).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// Ruby config
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — Ruby', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(rubyConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Ruby;
  mockContext.filePath = 'test.rb';

  it('extracts multiple fields from attr_accessor', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class User
  attr_accessor :name, :email, :age
end`);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();
    expect(extractor.isTypeDeclaration(classNode!)).toBe(true);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('User');
    expect(result!.fields).toHaveLength(3);

    const names = result!.fields.map((f) => f.name);
    expect(names).toContain('name');
    expect(names).toContain('email');
    expect(names).toContain('age');

    // All attr_accessor fields are public
    for (const field of result!.fields) {
      expect(field.visibility).toBe('public');
      expect(field.type).toBeNull();
    }
  });

  it('extracts fields from attr_reader as readonly', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class Config
  attr_reader :host, :port
end`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(2);
    expect(result!.fields[0].isReadonly).toBe(true);
    expect(result!.fields[1].isReadonly).toBe(true);
  });

  it('extracts fields from attr_writer as non-readonly', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class Settings
  attr_writer :theme
end`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(1);
    expect(result!.fields[0].name).toBe('theme');
    expect(result!.fields[0].isReadonly).toBe(false);
  });

  it('handles multiple attr_* calls in the same class', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class Person
  attr_accessor :name
  attr_reader :id
  attr_writer :password
end`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(3);

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.isReadonly).toBe(false);

    const idField = result!.fields.find((f) => f.name === 'id');
    expect(idField).toBeDefined();
    expect(idField!.isReadonly).toBe(true);

    const pwField = result!.fields.find((f) => f.name === 'password');
    expect(pwField).toBeDefined();
    expect(pwField!.isReadonly).toBe(false);
  });

  it('reports type as null (Ruby is dynamically typed)', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class Item
  attr_accessor :value
end`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields).toHaveLength(1);
    expect(result!.fields[0].type).toBeNull();
  });

  it('reports isStatic as false', () => {
    parser.setLanguage(Ruby);
    const tree = parser.parse(`class Demo
  attr_accessor :data
end`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    expect(result!.fields[0].isStatic).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// C# config — primary constructor fields
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — C# primary constructor fields', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(csharpFieldConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.CSharp;
  mockContext.filePath = 'test.cs';

  it('extracts record positional parameters as public readonly fields', () => {
    parser.setLanguage(CSharp);
    const tree = parser.parse('public record Person(string Name, int Age);');
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('Person');
    expect(result!.fields).toHaveLength(2);

    const nameField = result!.fields.find((f) => f.name === 'Name');
    expect(nameField).toBeDefined();
    expect(nameField!.type).toBe('string');
    expect(nameField!.visibility).toBe('public');
    expect(nameField!.isReadonly).toBe(true);
    expect(nameField!.isStatic).toBe(false);

    const ageField = result!.fields.find((f) => f.name === 'Age');
    expect(ageField).toBeDefined();
    expect(ageField!.type).toBe('int');
    expect(ageField!.visibility).toBe('public');
    expect(ageField!.isReadonly).toBe(true);
  });

  it('extracts class primary constructor parameters as private fields', () => {
    parser.setLanguage(CSharp);
    const tree = parser.parse(`public class Point(int x, int y) {
      public int X => x;
    }`);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();

    const xField = result!.fields.find((f) => f.name === 'x');
    expect(xField).toBeDefined();
    expect(xField!.type).toBe('int');
    expect(xField!.visibility).toBe('private');
    expect(xField!.isReadonly).toBe(false);
  });

  it('combines body fields with primary constructor fields', () => {
    parser.setLanguage(CSharp);
    const tree = parser.parse(`public class Service(string name) {
      public int Count;
    }`);
    const classNode = tree.rootNode.child(0);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    // Body field + primary constructor field
    expect(result!.fields.length).toBeGreaterThanOrEqual(2);

    const nameField = result!.fields.find((f) => f.name === 'name');
    expect(nameField).toBeDefined();
    expect(nameField!.visibility).toBe('private');

    const countField = result!.fields.find((f) => f.name === 'Count');
    expect(countField).toBeDefined();
  });
});

// ---------------------------------------------------------------------------
// Dart config — F26: static const / static final class fields
// ---------------------------------------------------------------------------

describe('GenericFieldExtractor — Dart', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(dartConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Dart;
  mockContext.filePath = 'test.dart';

  it('extracts a static const field as static + readonly (F26)', () => {
    parser.setLanguage(Dart);
    const tree = parser.parse(`class C {
  static const a = 1;
}`);
    const classNode = tree.rootNode.child(0);
    expect(classNode).toBeDefined();
    expect(extractor.isTypeDeclaration(classNode!)).toBe(true);

    const result = extractor.extract(classNode!, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('C');
    const a = result!.fields.find((f) => f.name === 'a');
    expect(a).toBeDefined();
    expect(a!.isStatic).toBe(true);
    expect(a!.isReadonly).toBe(true);
    expect(a!.visibility).toBe('public');
  });

  it('extracts multi-name static final fields, all static + readonly (F26)', () => {
    parser.setLanguage(Dart);
    const tree = parser.parse(`class C {
  static final String b = 'x', c = 'y';
}`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    // Exact-count guard (#1919 review CF4): `find()` below passes even on a
    // double-emit, so assert b/c surface exactly twice total — one field each,
    // no duplicate from the static_final_declaration_list multi-name path.
    expect(result!.fields.filter((f) => f.name === 'b' || f.name === 'c')).toHaveLength(2);
    const b = result!.fields.find((f) => f.name === 'b');
    const c = result!.fields.find((f) => f.name === 'c');
    expect(b).toBeDefined();
    expect(c).toBeDefined();
    for (const f of [b!, c!]) {
      expect(f.isStatic).toBe(true);
      expect(f.isReadonly).toBe(true);
      expect(f.type).toBe('String');
    }
  });

  it('still extracts instance fields (regression)', () => {
    parser.setLanguage(Dart);
    const tree = parser.parse(`class C {
  int z = 0;
}`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    const z = result!.fields.find((f) => f.name === 'z');
    expect(z).toBeDefined();
    expect(z!.isStatic).toBe(false);
    expect(z!.isReadonly).toBe(false);
    expect(z!.type).toBe('int');
  });

  it('marks an underscore-prefixed static const as private (F26)', () => {
    parser.setLanguage(Dart);
    const tree = parser.parse(`class C {
  static const _p = 1;
}`);
    const classNode = tree.rootNode.child(0);
    const result = extractor.extract(classNode!, mockContext);

    expect(result).not.toBeNull();
    const p = result!.fields.find((f) => f.name === '_p');
    expect(p).toBeDefined();
    expect(p!.visibility).toBe('private');
    expect(p!.isStatic).toBe(true);
    expect(p!.isReadonly).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// Kotlin config — F52: companion-object properties indexed as fields
// ---------------------------------------------------------------------------

const describeKotlin = Kotlin ? describe : describe.skip;

describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(kotlinConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Kotlin;
  mockContext.filePath = 'test.kt';

  /** The first companion_object node in `src`. */
  function companion(src: string): Parser.SyntaxNode {
    parser.setLanguage(Kotlin as Parser.Language);
    const tree = parser.parse(src);
    let found: Parser.SyntaxNode | undefined;
    const walk = (n: Parser.SyntaxNode) => {
      if (n.type === 'companion_object') found ??= n;
      for (let i = 0; i < n.namedChildCount; i++) {
        const c = n.namedChild(i);
        if (c) walk(c);
      }
    };
    walk(tree.rootNode);
    if (!found) throw new Error('no companion_object found');
    return found;
  }

  it('extracts a `const val` companion property as a static, readonly field', () => {
    const node = companion(`class C {
  companion object {
    const val TAG = "c"
  }
}`);
    expect(extractor.isTypeDeclaration(node)).toBe(true);
    const result = extractor.extract(node, mockContext);
    expect(result).not.toBeNull();
    const tag = result!.fields.find((f) => f.name === 'TAG');
    expect(tag).toBeDefined();
    expect(tag!.isStatic).toBe(true);
    expect(tag!.isReadonly).toBe(true);
  });

  it('extracts a property from a NAMED companion object', () => {
    const node = companion(`class C {
  companion object Factory {
    val x = 1
  }
}`);
    const result = extractor.extract(node, mockContext);
    const x = result!.fields.find((f) => f.name === 'x');
    expect(x).toBeDefined();
    expect(x!.isStatic).toBe(true);
    expect(x!.isReadonly).toBe(true);
  });

  it('indexes only the property, not the function, and emits it exactly once', () => {
    const node = companion(`class C {
  companion object {
    val onlyField = 1
    fun create() {}
  }
}`);
    const result = extractor.extract(node, mockContext);
    const fieldNames = result!.fields.map((f) => f.name);
    expect(fieldNames).toEqual(['onlyField']); // function excluded, no duplication
  });

  // CF4 (#1919 review): guard the new `isInsideKotlinCompanion` walk against
  // false-positives — a plain (non-companion) class property must be isStatic=false.
  it('reports a plain non-companion class property as isStatic=false (CF4)', () => {
    const classNode = firstNodeOfType(
      `class C {
  val x: Int = 1
}`,
      'class_declaration',
    );
    expect(extractor.isTypeDeclaration(classNode)).toBe(true);
    const result = extractor.extract(classNode, mockContext);
    expect(result).not.toBeNull();
    const x = result!.fields.find((f) => f.name === 'x');
    expect(x).toBeDefined();
    expect(x!.isStatic).toBe(false);
  });

  /** Parse `src` and return the first node of the given type (depth-first). */
  function firstNodeOfType(src: string, type: string): Parser.SyntaxNode {
    parser.setLanguage(Kotlin as Parser.Language);
    const tree = parser.parse(src);
    let found: Parser.SyntaxNode | undefined;
    const walk = (n: Parser.SyntaxNode) => {
      if (found) return;
      if (n.type === type) {
        found = n;
        return;
      }
      for (let i = 0; i < n.namedChildCount; i++) {
        const c = n.namedChild(i);
        if (c) walk(c);
      }
    };
    walk(tree.rootNode);
    if (!found) throw new Error(`no ${type} found`);
    return found;
  }
});

// ---------------------------------------------------------------------------
// Swift config — F75: protocol property requirements extracted as fields
// ---------------------------------------------------------------------------

const describeSwift = Swift ? describe : describe.skip;

describeSwift('GenericFieldExtractor — Swift (F75 protocol property requirements)', () => {
  const parser = new Parser();
  const extractor = createFieldExtractor(swiftConfig);
  const mockContext = createMockContext();
  mockContext.language = SupportedLanguages.Swift;
  mockContext.filePath = 'test.swift';

  /** Parse `src` and return the first class/protocol declaration node. */
  function declNode(src: string): Parser.SyntaxNode {
    parser.setLanguage(Swift as Parser.Language);
    const tree = parser.parse(src);
    const node = tree.rootNode.child(0);
    if (!node) throw new Error('no declaration node');
    return node;
  }

  it('extracts a `{ get }` protocol property requirement as a field (F75)', () => {
    const node = declNode(`protocol P {
  var title: String { get }
}`);
    expect(extractor.isTypeDeclaration(node)).toBe(true);
    const result = extractor.extract(node, mockContext);
    expect(result).not.toBeNull();
    expect(result!.ownerFqn).toBe('P');
    const title = result!.fields.find((f) => f.name === 'title');
    expect(title).toBeDefined();
    expect(title!.type).toBe('String');
    expect(title!.isStatic).toBe(false);
  });

  it('extracts a `{ get set }` protocol property requirement (F75)', () => {
    const node = declNode(`protocol P {
  var count: Int { get set }
}`);
    const result = extractor.extract(node, mockContext);
    const count = result!.fields.find((f) => f.name === 'count');
    expect(count).toBeDefined();
    expect(count!.type).toBe('Int');
  });

  it('extracts a static protocol property requirement as static (F75)', () => {
    const node = declNode(`protocol P {
  static var shared: P { get }
}`);
    const result = extractor.extract(node, mockContext);
    const shared = result!.fields.find((f) => f.name === 'shared');
    expect(shared).toBeDefined();
    expect(shared!.type).toBe('P');
    expect(shared!.isStatic).toBe(true);
  });

  it('extracts all requirements from a multi-property protocol (F75)', () => {
    const node = declNode(`protocol P {
  var title: String { get }
  var count: Int { get set }
  static var shared: P { get }
}`);
    const result = extractor.extract(node, mockContext);
    const names = result!.fields.map((f) => f.name).sort();
    expect(names).toEqual(['count', 'shared', 'title']);
  });

  it('still extracts a class stored property exactly once (regression)', () => {
    const node = declNode(`class C {
  var name: String = ""
}`);
    expect(extractor.isTypeDeclaration(node)).toBe(true);
    const result = extractor.extract(node, mockContext);
    const matches = result!.fields.filter((f) => f.name === 'name');
    expect(matches).toHaveLength(1);
    expect(matches[0].type).toBe('String');
  });
});
