import { describe, expect, it } from 'vitest';
import { EXPRESSION_KEYS, expressionStringsIn, firstExpressionError } from './spec-expressions';

/** The substring a span points at, to assert the offset/length land on the text. */
function slice(text: string, span: { offset: number; length: number }): string {
  return text.slice(span.offset, span.offset + span.length);
}

describe('spec-expressions', () => {
  it('declares the expression-bearing keys', () => {
    expect([...EXPRESSION_KEYS].sort()).toEqual(['calculate', 'expr', 'filter', 'test']);
  });

  it('finds a calculate expression and points between the quotes', () => {
    const text = '{ "transform": [ { "calculate": "datum.a + 1", "as": "b" } ] }';
    const spans = expressionStringsIn(text);
    expect(spans).toHaveLength(1);
    expect(spans[0].key).toBe('calculate');
    expect(spans[0].value).toBe('datum.a + 1');
    expect(slice(text, spans[0])).toBe('datum.a + 1');
  });

  it('finds string filter and test, in source order', () => {
    const text =
      '{ "transform": [ { "filter": "datum.x > 0" } ], "encoding": { "color": { "condition": { "test": "datum.x > 5", "value": "red" } } } }';
    const spans = expressionStringsIn(text);
    expect(spans.map((s) => s.value)).toEqual(['datum.x > 0', 'datum.x > 5']);
  });

  it('excludes object-form filter predicates (only string values are expressions)', () => {
    const text =
      '{ "transform": [ { "filter": { "field": "x", "gt": 0 } }, { "filter": { "param": "brush" } } ] }';
    expect(expressionStringsIn(text)).toEqual([]);
  });

  it('finds an expr value (param / value-ref form)', () => {
    const text = '{ "params": [ { "name": "n", "expr": "width / 2" } ] }';
    const spans = expressionStringsIn(text);
    expect(spans.map((s) => s.value)).toEqual(['width / 2']);
  });

  it('finds expressions across all views of a composition', () => {
    const text =
      '{ "hconcat": [ { "transform": [ { "calculate": "datum.a", "as": "a2" } ] }, { "transform": [ { "calculate": "datum.b", "as": "b2" } ] } ] }';
    expect(expressionStringsIn(text).map((s) => s.value)).toEqual(['datum.a', 'datum.b']);
  });

  it('keeps offsets aligned when the string contains escapes', () => {
    const text = '{ "transform": [ { "calculate": "datum[\\"a b\\"]", "as": "c" } ] }';
    const spans = expressionStringsIn(text);
    expect(spans).toHaveLength(1);
    // The raw inner token (escapes intact) is what the span covers.
    expect(slice(text, spans[0])).toBe('datum[\\"a b\\"]');
  });

  it('yields a zero-length span for an empty expression', () => {
    const text = '{ "transform": [ { "calculate": "", "as": "c" } ] }';
    const spans = expressionStringsIn(text);
    expect(spans).toHaveLength(1);
    expect(spans[0].length).toBe(0);
    expect(spans[0].value).toBe('');
  });

  it('returns nothing for unparseable text rather than throwing', () => {
    expect(expressionStringsIn('{ "transform": [ { "calc')).toEqual([]);
  });
});

describe('firstExpressionError', () => {
  it('returns null when every expression parses', () => {
    const text = '{ "transform": [ { "filter": "datum.value > 0" } ] }';
    expect(firstExpressionError(text)).toBeNull();
  });

  it('attributes a malformed expression to its property, with the parser message', () => {
    const text = '{ "transform": [ { "filter": "datum[\'value\'] ==" } ] }';
    const err = firstExpressionError(text);
    expect(err?.key).toBe('filter');
    expect(err?.expr).toBe("datum['value'] ==");
    expect(err?.message).toBe('Unexpected end of input');
  });

  it('reports the 1-based line of the offending expression', () => {
    const text = ['{', '  "transform": [', '    { "calculate": "clamp(" }', '  ]', '}'].join('\n');
    expect(firstExpressionError(text)?.line).toBe(3);
  });

  it('ignores empty expressions (incomplete, not malformed)', () => {
    expect(firstExpressionError('{ "transform": [ { "calculate": "", "as": "x" } ] }')).toBeNull();
  });

  it('returns the first malformed expression in document order', () => {
    const text =
      '{ "transform": [ { "calculate": "datum.a +", "as": "x" }, { "filter": "datum.b ==" } ] }';
    expect(firstExpressionError(text)?.key).toBe('calculate');
  });
});
