import { describe, expect, it } from 'vitest';
import {
  appendEntryEdit,
  arrayAffixes,
  createArrayPropertyEdit,
  snippetToPlain,
  type SnippetEdit,
} from './spec-snippet';

/** Apply a computed edit to the text, tab stops stripped to their defaults. */
function apply(text: string, edit: SnippetEdit): string {
  return text.slice(0, edit.offset) + snippetToPlain(edit.snippet) + text.slice(edit.offset);
}

describe('snippetToPlain', () => {
  it('reduces tab stops to their default text', () => {
    expect(snippetToPlain('"a": "${1:x}", "b": ${2:true}')).toBe('"a": "x", "b": true');
  });

  it('drops an empty final tab stop, braced or bare', () => {
    expect(snippetToPlain('foo${0}')).toBe('foo');
    expect(snippetToPlain('[$0]')).toBe('[]');
  });
});

describe('arrayAffixes — keeps a spliced-in element valid JSON', () => {
  const at = (marked: string) => {
    const i = marked.indexOf('|');
    return arrayAffixes(marked.replace('|', ''), i, i);
  };
  it('no commas in an empty array', () => {
    expect(at('[|]')).toEqual({ lead: '', trail: '' });
  });
  it('leading comma when appended after an element', () => {
    expect(at('[{"a":1}|]')).toEqual({ lead: ', ', trail: '' });
  });
  it('no leading comma right after a separator comma', () => {
    expect(at('[{"a":1},|]')).toEqual({ lead: '', trail: '' });
  });
  it('trailing comma when inserted before an element', () => {
    expect(at('[|{"a":1}]')).toEqual({ lead: '', trail: ',' });
  });
  it('both when squeezed between two elements without commas', () => {
    expect(at('[{"a":1}|{"b":2}]')).toEqual({ lead: ', ', trail: ',' });
  });
});

describe('appendEntryEdit — applied, the array parses with the entry last', () => {
  /** Append into the `transform` array of `doc` and parse the result. */
  function appended(doc: string): unknown[] {
    const arrayOffset = doc.indexOf('[');
    const array = { offset: arrayOffset, length: doc.lastIndexOf(']') - arrayOffset + 1 };
    const out = apply(doc, appendEntryEdit(doc, array, '{ "x": ${1:9} }'));
    return (JSON.parse(out) as { t: unknown[] }).t;
  }

  it('into an empty array', () => {
    expect(appended('{"t":[]}')).toEqual([{ x: 9 }]);
  });

  it('after an element with no trailing comma', () => {
    expect(appended('{"t":[{"a":1}]}')).toEqual([{ a: 1 }, { x: 9 }]);
  });

  it('across a multi-line formatted array', () => {
    expect(appended('{"t":[\n  { "a": 1 },\n  { "b": 2 }\n]}')).toEqual([
      { a: 1 },
      { b: 2 },
      { x: 9 },
    ]);
  });
});

describe('createArrayPropertyEdit — applied, the object parses with the new property', () => {
  it('adds a comma before a following property', () => {
    const doc = '{\n  "mark": "bar"\n}';
    const out = apply(doc, createArrayPropertyEdit(doc, 0, 2, 'params', '{ "n": ${1:1} }'));
    expect(JSON.parse(out)).toEqual({ params: [{ n: 1 }], mark: 'bar' });
  });

  it('takes no comma in an otherwise-empty object', () => {
    const doc = '{}';
    const out = apply(doc, createArrayPropertyEdit(doc, 0, 2, 'transform', '$0'));
    expect(JSON.parse(out)).toEqual({ transform: [] });
    expect(out).toBe('{\n  "transform": []}');
  });

  it('indents off the brace line, not the brace column (compact-formatted host)', () => {
    const doc = '{\n  "hconcat": [\n    { "mark": "bar" }\n  ]\n}';
    const brace = doc.indexOf('{ "mark"');
    const edit = createArrayPropertyEdit(doc, brace, 2, 'params', '{ "n": ${1:1} }');
    // One tab past the line's 4-space indent, not past the mid-line brace column.
    expect(edit.snippet.startsWith('\n      "params"')).toBe(true);
    expect(JSON.parse(apply(doc, edit))).toEqual({
      hconcat: [{ params: [{ n: 1 }], mark: 'bar' }],
    });
  });

  it('honors the tab size', () => {
    const edit = createArrayPropertyEdit('{}', 0, 4, 'params', '$0');
    expect(edit.snippet.startsWith('\n    "params"')).toBe(true);
  });
});
