import { describe, expect, it } from 'vitest';
import { findViewRange, valueKeyAtOffset } from './spec-cursor';

/** Parse the slice a range points at, for asserting which view was targeted. */
function sliceObject(text: string, range: { offset: number; length: number }) {
  return JSON.parse(text.slice(range.offset, range.offset + range.length)) as Record<
    string,
    unknown
  >;
}

const layered = JSON.stringify(
  {
    $schema: 'https://vega.github.io/schema/vega-lite/v6.json',
    description: 'A simple bar chart.',
    data: { values: [{ category: 'A', value: 28 }] },
    layer: [
      {
        name: 'outer1',
        mark: 'bar',
        encoding: {
          x: { field: 'category', type: 'nominal' },
          y: { field: 'value', type: 'quantitative' },
        },
      },
      { mark: 'point', encoding: {}, name: 'outer2' },
    ],
  },
  null,
  2,
);

describe('findViewRange', () => {
  it('targets the layer element the cursor sits in, not the whole doc', () => {
    const at = layered.indexOf('"outer2"'); // cursor inside the second layer element
    const range = findViewRange(layered, at)!;
    expect(range).not.toBeNull();
    expect(sliceObject(layered, range).name).toBe('outer2');
  });

  it('climbs out of a deeply nested property to the enclosing view', () => {
    const at = layered.indexOf('"nominal"'); // deep inside outer1's x encoding
    const range = findViewRange(layered, at)!;
    expect(sliceObject(layered, range).name).toBe('outer1');
  });

  it('returns null at a top-level key (caller wraps the whole document)', () => {
    const at = layered.indexOf('"description"');
    expect(findViewRange(layered, at)).toBeNull();
  });

  it('returns null for a flat unit spec (no inner view)', () => {
    const unit = JSON.stringify(
      { mark: 'bar', encoding: { x: { field: 'a', type: 'nominal' } } },
      null,
      2,
    );
    expect(findViewRange(unit, unit.indexOf('"field"'))).toBeNull();
  });

  it('targets the facet/repeat child spec', () => {
    const faceted = JSON.stringify(
      { facet: { field: 'g', type: 'nominal' }, spec: { mark: 'bar', encoding: {} } },
      null,
      2,
    );
    const range = findViewRange(faceted, faceted.indexOf('"bar"'))!;
    expect(sliceObject(faceted, range).mark).toBe('bar');
  });

  it('still resolves a complete inner view while the outer doc is mid-edit', () => {
    // Trailing junk makes the whole document unparseable; the inner object is intact.
    const broken = layered.replace(/}\s*$/, '} ,');
    const at = broken.indexOf('"outer2"');
    const range = findViewRange(broken, at)!;
    expect(sliceObject(broken, range).name).toBe('outer2');
  });
});

describe('valueKeyAtOffset', () => {
  const spec = JSON.stringify(
    { mark: 'bar', encoding: { x: { field: 'category', type: 'nominal' } }, transform: [] },
    null,
    2,
  );

  it('reports the property whose value the cursor is in', () => {
    // inside the "category" value of "field": "category"
    const at = spec.indexOf('category') + 2;
    expect(valueKeyAtOffset(spec, at)).toBe('field');
  });

  it('reports the array key for an element position', () => {
    const grouped = JSON.stringify(
      { transform: [{ aggregate: [], groupby: ['a', 'b'] }] },
      null,
      2,
    );
    const at = grouped.indexOf('"b"') + 1;
    expect(valueKeyAtOffset(grouped, at)).toBe('groupby');
  });

  it('is null on a property key, not its value', () => {
    const at = spec.indexOf('"field"') + 1; // on the key itself
    expect(valueKeyAtOffset(spec, at)).toBe(null);
  });
});
