import { describe, it, expect } from 'vitest';
import { validateExpression, referencedFields, activeCall } from './expr-validate';

describe('validateExpression', () => {
  it('accepts a well-formed Vega expression', () => {
    expect(validateExpression('datum.price * datum.qty').valid).toBe(true);
    expect(validateExpression("datum.region === 'East' && datum.value > 0").valid).toBe(true);
  });

  it('treats an empty / whitespace expression as valid (incomplete, not a mistake)', () => {
    expect(validateExpression('').valid).toBe(true);
    expect(validateExpression('   ').valid).toBe(true);
  });

  it('rejects a malformed expression with a message', () => {
    const result = validateExpression('datum.price *');
    expect(result.valid).toBe(false);
    expect(result.error).toBeTruthy();
  });
});

describe('referencedFields', () => {
  it('collects datum.field and datum["field"] references, de-duplicated in order', () => {
    expect(referencedFields("datum.price * datum['qty'] + datum.price")).toEqual(['price', 'qty']);
  });

  it('ignores function calls and non-datum identifiers', () => {
    expect(referencedFields('toNumber(datum.amount) + PI')).toEqual(['amount']);
  });

  it('skips dynamic (computed, non-literal) access', () => {
    expect(referencedFields('datum[someVar]')).toEqual([]);
  });

  it('returns nothing for an empty or unparseable expression', () => {
    expect(referencedFields('')).toEqual([]);
    expect(referencedFields('datum.price *')).toEqual([]);
  });
});

describe('activeCall', () => {
  it('returns null when the cursor is not inside any call', () => {
    expect(activeCall('datum.price + ')).toBeNull();
    expect(activeCall('')).toBeNull();
  });

  it('names the enclosing call and reports the first argument', () => {
    expect(activeCall('clamp(')).toEqual({ name: 'clamp', activeParam: 0 });
    expect(activeCall('if(datum.x > 0')).toEqual({ name: 'if', activeParam: 0 });
  });

  it('counts commas to find the active argument', () => {
    expect(activeCall('clamp(datum.x, 0, ')).toEqual({ name: 'clamp', activeParam: 2 });
  });

  it('reports the innermost call when calls are nested', () => {
    expect(activeCall('if(datum.x > 0, min(1, ')).toEqual({ name: 'min', activeParam: 1 });
  });

  it('keeps a nested array argument on its outer call argument', () => {
    expect(activeCall('clamp(datum.x, [1, 2')).toEqual({ name: 'clamp', activeParam: 1 });
  });

  it('does not count commas inside string literals', () => {
    expect(activeCall("if(test(regexp('a,b'), datum.s), ")).toEqual({ name: 'if', activeParam: 1 });
  });

  it('treats a bare grouping paren as not a call', () => {
    expect(activeCall('(datum.x + 1) * ')).toBeNull();
  });
});
