import { describe, it, expect } from 'vitest';
import { parse } from 'yaml';
import { patchYaml, diff } from './yaml-patch.js';

// Representative piece with all the formatting we want to preserve.
const ORIGINAL = [
  'name: sample',
  'description: |',
  '  Multi-line',
  '  description.',
  'max_movements: 10',
  'initial_movement: start',
  '',
  'movements:',
  '  - name: start',
  '    edit: false',
  '    persona: analyst',
  '    instruction: |',
  '      Do the thing.',
  '      Keep newlines.',
  '    allowed_tools: [Read, Glob]',
  '    default_next: execute',
  '    rules:',
  '      - condition: ok',
  '        next: execute',
  '',
  '  - name: execute',
  '    edit: true',
  '    persona: worker',
  '    instruction: |',
  '      Work work work.',
  '    allowed_tools: [Read, Write, Edit]',
  '    default_next: verify',
  '    rules:',
  '      - condition: done',
  '        next: verify',
  '',
  '  - name: verify',
  '    edit: false',
  '    persona: reviewer',
  '    instruction: |',
  '      Review it.',
  '    allowed_tools: [Read]',
  '    default_next: COMPLETE',
  '    rules:',
  '      - condition: good',
  '        next: COMPLETE',
  '',
].join('\n');

describe('diff', () => {
  it('returns no ops when values are equal', () => {
    expect(diff({ a: 1, b: [1, 2] }, { a: 1, b: [1, 2] })).toEqual([]);
  });

  it('emits a single set for a changed leaf', () => {
    const ops = diff({ a: 1, b: 2 }, { a: 1, b: 3 });
    expect(ops).toEqual([{ kind: 'set', path: ['b'], value: 3 }]);
  });

  it('emits a delete for a removed key', () => {
    const ops = diff({ a: 1, b: 2 }, { a: 1 });
    expect(ops).toEqual([{ kind: 'delete', path: ['b'] }]);
  });

  it('emits a set for a new key', () => {
    const ops = diff({ a: 1 }, { a: 1, b: 2 });
    expect(ops).toEqual([{ kind: 'set', path: ['b'], value: 2 }]);
  });

  it('recurses element-wise when array lengths match', () => {
    const ops = diff({ xs: [1, 2, 3] }, { xs: [1, 2, 4] });
    expect(ops).toEqual([{ kind: 'set', path: ['xs', 2], value: 4 }]);
  });

  it('replaces whole array when lengths differ', () => {
    const ops = diff({ xs: [1, 2, 3] }, { xs: [1, 2, 3, 4] });
    expect(ops).toEqual([{ kind: 'set', path: ['xs'], value: [1, 2, 3, 4] }]);
  });
});

describe('patchYaml', () => {
  it('preserves `|` literal block style when editing instruction text', () => {
    const body = parse(ORIGINAL);
    body.movements[0].instruction = 'Do the updated thing.\nStill multiline.\n';
    const patched = patchYaml(ORIGINAL, body);
    expect(patched).toContain('instruction: |');
    expect(patched).not.toMatch(/instruction:\s*>/);
    // round-trips to the intended value
    expect(parse(patched).movements[0].instruction).toBe(
      'Do the updated thing.\nStill multiline.\n',
    );
  });

  it('does not reformat unrelated inline allowed_tools array', () => {
    const body = parse(ORIGINAL);
    body.movements[0].instruction = 'tweaked';
    const patched = patchYaml(ORIGINAL, body);
    // yaml v2 may normalize bracket spacing (`[x]` -> `[ x ]`), but the key
    // invariant is that flow-style stays flow (no multiline block expansion).
    expect(patched).toMatch(/allowed_tools: \[\s*Read,\s*Write,\s*Edit\s*\]/);
    expect(patched).toMatch(/allowed_tools: \[\s*Read\s*\]/);
    // And crucially it must NOT have been expanded to block style:
    expect(patched).not.toMatch(/allowed_tools:\s*\n\s*-\s*Read\s*\n\s*-\s*Write/);
  });

  it('preserves blank lines between movements', () => {
    const body = parse(ORIGINAL);
    body.movements[1].persona = 'coder';
    const patched = patchYaml(ORIGINAL, body);
    // Each movement in the original is separated by a blank line. Check that
    // the sequence `\n\n  - name:` still appears between them.
    const blankLineBeforeMovements = patched.match(/\n\n  - name:/g);
    expect(blankLineBeforeMovements).not.toBeNull();
    expect(blankLineBeforeMovements!.length).toBeGreaterThanOrEqual(2);
  });

  it('supports adding a brand-new movement', () => {
    const body = parse(ORIGINAL);
    body.movements.push({
      name: 'extra',
      edit: false,
      persona: 'helper',
      instruction: 'added step',
      allowed_tools: ['Read'],
      default_next: 'COMPLETE',
      rules: [{ condition: 'x', next: 'COMPLETE' }],
    });
    const patched = patchYaml(ORIGINAL, body);
    const reparsed = parse(patched);
    expect(reparsed.movements).toHaveLength(4);
    expect(reparsed.movements[3].name).toBe('extra');
    // other movements still have their literal-block instructions intact
    expect(patched).toContain('instruction: |');
  });

  it('supports deleting a movement', () => {
    const body = parse(ORIGINAL);
    body.movements.splice(1, 1); // drop `execute`
    const patched = patchYaml(ORIGINAL, body);
    const reparsed = parse(patched);
    expect(reparsed.movements).toHaveLength(2);
    expect(reparsed.movements.map((m: any) => m.name)).toEqual(['start', 'verify']);
  });

  it('renaming default_next is a minimal targeted change', () => {
    const body = parse(ORIGINAL);
    body.movements[0].default_next = 'verify';
    const patched = patchYaml(ORIGINAL, body);
    expect(parse(patched).movements[0].default_next).toBe('verify');
    // untouched: instruction block style, inline arrays, blank lines
    expect(patched).toContain('instruction: |');
    expect(patched).toMatch(/allowed_tools: \[\s*Read,\s*Glob\s*\]/);
    expect(patched).toMatch(/allowed_tools: \[\s*Read,\s*Write,\s*Edit\s*\]/);
    expect(patched).toMatch(/\n\n  - name: execute/);
  });

  it('falls back to stringify when original yaml is malformed', () => {
    const broken = 'name: x\n  bad-indent: [\n';
    const body = { name: 'x', description: 'ok' };
    const out = patchYaml(broken, body);
    // Fallback should still produce parseable output with the new body.
    const parsed = parse(out);
    expect(parsed.name).toBe('x');
    expect(parsed.description).toBe('ok');
  });

  it('semantic content matches body after patching', () => {
    const body = parse(ORIGINAL);
    body.description = 'new description';
    body.movements[2].instruction = 'reviewed differently';
    body.movements[2].allowed_tools = ['Read', 'Glob'];
    const patched = patchYaml(ORIGINAL, body);
    expect(parse(patched)).toEqual(body);
  });
});
