import { describe, expect, it } from 'vitest';
import { viewTree, type ViewNode } from './spec-view-tree';

const json = (o: unknown) => JSON.stringify(o, null, 2);

describe('viewTree', () => {
  it('returns a single unit node for a flat spec, with its mark', () => {
    const t = viewTree(json({ mark: 'bar', encoding: {} }))!;
    expect(t.kind).toBe('unit');
    expect(t.mark).toBe('bar');
    expect(t.children).toHaveLength(0);
  });

  it('reads mark from an object mark (mark.type)', () => {
    const t = viewTree(json({ mark: { type: 'line', point: true } }))!;
    expect(t.mark).toBe('line');
  });

  it('builds an array composition with per-element paths and orientation', () => {
    const t = viewTree(json({ vconcat: [{ mark: 'point' }, { mark: 'bar' }] }))!;
    expect(t.kind).toBe('composition');
    expect(t.op).toBe('vconcat');
    expect(t.orientation).toBe('vertical');
    expect(t.children.map((c) => c.path)).toEqual([
      ['vconcat', 0],
      ['vconcat', 1],
    ]);
    expect(t.children.map((c) => c.mark)).toEqual(['point', 'bar']);
  });

  it('recurses into nested compositions', () => {
    const t = viewTree(
      json({ vconcat: [{ mark: 'point' }, { hconcat: [{ mark: 'bar' }, { mark: 'line' }] }] }),
    )!;
    const nested = t.children[1];
    expect(nested.kind).toBe('composition');
    expect(nested.op).toBe('hconcat');
    expect(nested.orientation).toBe('horizontal');
    expect(nested.children.map((c) => c.path)).toEqual([
      ['vconcat', 1, 'hconcat', 0],
      ['vconcat', 1, 'hconcat', 1],
    ]);
  });

  it('marks a layer as layered', () => {
    const t = viewTree(json({ layer: [{ mark: 'area' }, { mark: 'line' }] }))!;
    expect(t.op).toBe('layer');
    expect(t.orientation).toBe('layered');
    expect(t.children).toHaveLength(2);
  });

  it('treats facet/repeat as a composition with one `spec` child', () => {
    const facet = viewTree(
      json({ facet: { field: 'c', type: 'nominal' }, spec: { mark: 'bar' } }),
    )!;
    expect(facet.op).toBe('facet');
    expect(facet.children).toHaveLength(1);
    expect(facet.children[0].path).toEqual(['spec']);
    expect(facet.children[0].mark).toBe('bar');

    const repeat = viewTree(json({ repeat: ['a', 'b'], spec: { mark: 'point' } }))!;
    expect(repeat.op).toBe('repeat');
    expect(repeat.children[0].path).toEqual(['spec']);
  });

  it('exposes byte ranges that point at each node object', () => {
    const text = json({ vconcat: [{ mark: 'point' }, { mark: 'bar' }] });
    const t = viewTree(text)!;
    const child = t.children[1];
    expect(text[child.offset]).toBe('{'); // the element object's opening brace
    expect(text.slice(child.offset, child.offset + child.length)).toContain('"bar"');
  });

  it('returns null for empty or non-object input', () => {
    expect(viewTree('')).toBeNull();
    expect(viewTree('[1, 2]')).toBeNull();
  });

  it('is tolerant of a mid-edit trailing comma', () => {
    const t = viewTree('{ "vconcat": [ { "mark": "point" }, ] }');
    expect(t?.op).toBe('vconcat');
    expect((t as ViewNode).children.length).toBeGreaterThanOrEqual(1);
  });
});
