import { describe, expect, it } from 'vitest';
import {
  compositionTargetAt,
  elementOffset,
  insertView,
  moveView,
  type SpecPath,
} from './spec-insert';

const layered = JSON.stringify(
  {
    data: { name: 'd' },
    hconcat: [{ layer: [{ mark: 'bar' }, { mark: 'line' }] }, { mark: 'point' }],
  },
  null,
  2,
);

/** Offset of the first occurrence of `needle` in `text` (a cursor inside it). */
const at = (text: string, needle: string): number => text.indexOf(needle) + 1;

describe('compositionTargetAt', () => {
  it('finds the innermost composition and the element the cursor is on', () => {
    const target = compositionTargetAt(layered, at(layered, '"line"'))!;
    expect(target.arrayPath).toEqual(['hconcat', 0, 'layer']);
    expect(target.count).toBe(2);
    expect(target.element?.index).toBe(1);
  });

  it('targets the outer composition when the cursor is on its direct element', () => {
    const target = compositionTargetAt(layered, at(layered, '"point"'))!;
    expect(target.arrayPath).toEqual(['hconcat']);
    expect(target.element?.index).toBe(1);
  });

  it('reports no element when the cursor is not on one (non-empty array)', () => {
    const spec = '{ "vconcat": [\n  { "mark": "bar" }\n] }';
    const target = compositionTargetAt(spec, spec.indexOf('['))!;
    expect(target.count).toBe(1);
    expect(target.element).toBeNull();
  });

  it('reports an empty composition (count 0, no element) for the append fallback', () => {
    const spec = '{ "vconcat": [] }';
    const target = compositionTargetAt(spec, spec.indexOf('[') + 1)!;
    expect(target.arrayPath).toEqual(['vconcat']);
    expect(target.count).toBe(0);
    expect(target.element).toBeNull();
  });

  it('is null when the cursor is in a flat unit spec', () => {
    const spec = JSON.stringify({ mark: 'bar' });
    expect(compositionTargetAt(spec, at(spec, '"bar"'))).toBeNull();
  });
});

describe('insertView', () => {
  it('inserts an empty placeholder at the given index', () => {
    const spec = { hconcat: [{ mark: 'bar' }, { mark: 'point' }] };
    const out = insertView(spec, ['hconcat'], 1)!;
    expect(out.hconcat).toEqual([
      { mark: 'bar' },
      { mark: 'point', encoding: {} },
      { mark: 'point' },
    ]);
    expect(spec.hconcat).toHaveLength(2); // input untouched
  });

  it('clamps an out-of-range index to the array bounds', () => {
    const spec = { layer: [{ mark: 'bar' }] };
    expect(insertView(spec, ['layer'], 99)!.layer as unknown[]).toHaveLength(2);
    expect((insertView(spec, ['layer'], -5)!.layer as unknown[])[0]).toEqual({
      mark: 'point',
      encoding: {},
    });
  });

  it('reaches a nested composition array', () => {
    const spec = { hconcat: [{ layer: [{ mark: 'bar' }] }] };
    const out = insertView(spec, ['hconcat', 0, 'layer'], 1)!;
    expect((out.hconcat as { layer: unknown[] }[])[0].layer).toHaveLength(2);
  });

  it('returns null when the path is not an array', () => {
    expect(insertView({ mark: 'bar' }, ['layer'], 0)).toBeNull();
  });
});

describe('moveView', () => {
  it('swaps a view with its neighbor', () => {
    const spec = { vconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }] };
    expect(moveView(spec, ['vconcat'], 1, -1)!.vconcat).toEqual([
      { mark: 'b' },
      { mark: 'a' },
      { mark: 'c' },
    ]);
    expect(moveView(spec, ['vconcat'], 1, 1)!.vconcat).toEqual([
      { mark: 'a' },
      { mark: 'c' },
      { mark: 'b' },
    ]);
    expect(spec.vconcat[0]).toEqual({ mark: 'a' }); // input untouched
  });

  it('returns null at the edges', () => {
    const spec = { vconcat: [{ mark: 'a' }, { mark: 'b' }] };
    expect(moveView(spec, ['vconcat'], 0, -1)).toBeNull();
    expect(moveView(spec, ['vconcat'], 1, 1)).toBeNull();
  });

  it('returns null when the path is not an array', () => {
    expect(moveView({ mark: 'bar' }, ['layer'], 0, 1)).toBeNull();
  });
});

describe('elementOffset', () => {
  it('returns the start offset of the element at the path', () => {
    const path: SpecPath = ['hconcat', 0, 'layer'];
    const off = elementOffset(layered, path, 1)!;
    expect(layered.slice(off, off + 1)).toBe('{'); // the element object's brace
    expect(layered.slice(off)).toContain('"line"');
  });

  it('is null for an out-of-range index', () => {
    expect(elementOffset(layered, ['hconcat'], 9)).toBeNull();
  });
});
