import { describe, expect, it } from 'vitest';
import { simplifyStructure, wrapContainer, wrapViews } from './spec-restructure';

describe('wrapViews — wrap across the target axis', () => {
  it('pairs a view beside a sibling in a new perpendicular concat', () => {
    // The motivating case: a vconcat of three, drop the last beside the middle to
    // make a row — vconcat:[A, hconcat:[B, C]].
    const spec = { data: { name: 'd' }, vconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }] };
    const next = wrapViews(spec, ['vconcat', 1], ['vconcat', 2], 'horizontal', 'after');
    expect(next).toEqual({
      data: { name: 'd' },
      vconcat: [{ mark: 'a' }, { hconcat: [{ mark: 'b' }, { mark: 'c' }] }],
    });
    expect(spec.vconcat).toHaveLength(3); // input untouched
  });

  it('honors the side — source before the target', () => {
    // Wrapping A and removing B empties the hconcat to one child, which collapses,
    // so the result is the bare vconcat with the source ahead of the target.
    const spec = { hconcat: [{ mark: 'a' }, { mark: 'b' }] };
    const next = wrapViews(spec, ['hconcat', 0], ['hconcat', 1], 'vertical', 'before');
    expect(next).toEqual({ vconcat: [{ mark: 'b' }, { mark: 'a' }] });
  });
});

describe('wrapViews — flatten makes a with-axis drop an insert', () => {
  it('inserts into the target concat instead of nesting a redundant one', () => {
    // Drop S (from a different container) onto the bottom edge of B, which already
    // sits in a vconcat → S joins that vconcat after B, the source container collapses.
    const spec = {
      hconcat: [{ vconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 's' }],
    };
    const next = wrapViews(spec, ['hconcat', 0, 'vconcat', 1], ['hconcat', 1], 'vertical', 'after');
    expect(next).toEqual({ vconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 's' }] });
  });
});

describe('wrapViews — collapse the source container', () => {
  it('unwraps a single-child container left behind by the move', () => {
    const spec = { vconcat: [{ hconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 'c' }] };
    // Drag B out to sit below C (a with-axis drop → reorder via flatten), leaving the
    // hconcat with one child, which collapses to a bare unit.
    const next = wrapViews(spec, ['vconcat', 1], ['vconcat', 0, 'hconcat', 1], 'vertical', 'after');
    expect(next).toEqual({ vconcat: [{ mark: 'a' }, { mark: 'c' }, { mark: 'b' }] });
  });
});

describe('wrapViews — data pinning', () => {
  it('pins the source’s inherited data when it would otherwise rebind', () => {
    const spec = {
      data: { name: 'root' },
      vconcat: [{ mark: 'a' }, { data: { name: 'd2' }, hconcat: [{ mark: 'b' }, { mark: 'c' }] }],
    };
    const next = wrapViews(
      spec,
      ['vconcat', 0],
      ['vconcat', 1, 'hconcat', 0],
      'horizontal',
      'after',
    );
    expect(next).toEqual({
      data: { name: 'root' },
      vconcat: [
        { hconcat: [{ mark: 'a' }, { data: { name: 'd2' }, mark: 'b' }] },
        { data: { name: 'd2' }, mark: 'c' },
      ],
    });
  });

  it('adds no data key when the source already inherits the destination data', () => {
    const spec = { data: { name: 'd' }, vconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }] };
    const next = wrapViews(spec, ['vconcat', 0], ['vconcat', 2], 'horizontal', 'after');
    // C had no data and the whole spec shares one source, so nothing is pinned.
    expect(JSON.stringify(next)).not.toContain('"data":{"name":"d"},{"mark":"c"');
    expect(next).toEqual({
      data: { name: 'd' },
      vconcat: [{ hconcat: [{ mark: 'a' }, { mark: 'c' }] }, { mark: 'b' }],
    });
  });
});

describe('wrapViews — degenerate drops', () => {
  const spec = { vconcat: [{ hconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 'c' }] };

  it('returns null for a drop onto itself', () => {
    expect(wrapViews(spec, ['vconcat', 1], ['vconcat', 1], 'horizontal', 'after')).toBeNull();
  });

  it('returns null when the source is an ancestor of the target', () => {
    expect(
      wrapViews(spec, ['vconcat', 0, 'hconcat', 1], ['vconcat', 0], 'horizontal', 'after'),
    ).toBeNull();
  });

  it('returns null when the target is a descendant of the source', () => {
    expect(
      wrapViews(spec, ['vconcat', 0], ['vconcat', 0, 'hconcat', 0], 'vertical', 'after'),
    ).toBeNull();
  });

  it('returns null when wrapping the root', () => {
    expect(wrapViews(spec, [], ['vconcat', 1], 'horizontal', 'after')).toBeNull();
  });
});

describe('wrapContainer — pull a view out into a new full-span row/column', () => {
  it('pulls a view out of the root row into a new row above the rest', () => {
    // The motivating case: hconcat[A,B,C,D], drop A on the frame's top margin →
    // vconcat[A, hconcat[B,C,D]] — impossible with wrapViews (it pairs two views).
    const spec = {
      data: { name: 'd' },
      hconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }, { mark: 'd' }],
    };
    const next = wrapContainer(spec, [], ['hconcat', 0], 'vertical', 'before');
    expect(next).toEqual({
      data: { name: 'd' },
      vconcat: [{ mark: 'a' }, { hconcat: [{ mark: 'b' }, { mark: 'c' }, { mark: 'd' }] }],
    });
    expect(spec.hconcat).toHaveLength(4); // input untouched
  });

  it('keeps spec-level metadata on the wrapper when wrapping the root', () => {
    const spec = {
      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
      data: { name: 'd' },
      config: { view: { stroke: null } },
      hconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }],
    };
    const next = wrapContainer(spec, [], ['hconcat', 2], 'vertical', 'after');
    expect(next).toEqual({
      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
      data: { name: 'd' },
      config: { view: { stroke: null } },
      vconcat: [{ hconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 'c' }],
    });
  });

  it('pulls a view out of a vconcat into a new column beside it', () => {
    const spec = { vconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }] };
    const next = wrapContainer(spec, [], ['vconcat', 0], 'horizontal', 'before');
    expect(next).toEqual({
      hconcat: [{ mark: 'a' }, { vconcat: [{ mark: 'b' }, { mark: 'c' }] }],
    });
  });

  it('flattens into the parent when the new wrap matches the grandparent axis', () => {
    // Drop A on the inner hconcat's top margin: the new vconcat around it is bare and
    // same-orientation as the outer vconcat, so it flattens to a plain row above.
    const spec = {
      vconcat: [{ hconcat: [{ mark: 'a' }, { mark: 'b' }, { mark: 'c' }] }, { mark: 'x' }],
    };
    const next = wrapContainer(
      spec,
      ['vconcat', 0],
      ['vconcat', 0, 'hconcat', 0],
      'vertical',
      'before',
    );
    expect(next).toEqual({
      vconcat: [{ mark: 'a' }, { hconcat: [{ mark: 'b' }, { mark: 'c' }] }, { mark: 'x' }],
    });
  });

  it('collapses a two-child container down to the lone survivor', () => {
    // hconcat[A,B], pull A out above → the hconcat is left with one child, which
    // collapses, so the result is the bare vconcat of the two.
    const spec = { hconcat: [{ mark: 'a' }, { mark: 'b' }] };
    const next = wrapContainer(spec, [], ['hconcat', 0], 'vertical', 'before');
    expect(next).toEqual({ vconcat: [{ mark: 'a' }, { mark: 'b' }] });
  });

  it('pins the source’s inherited data when pulled across data contexts', () => {
    const spec = {
      data: { name: 'root' },
      vconcat: [{ data: { name: 'd2' }, hconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 'c' }],
    };
    // Pull A (which inherits d2) out to wrap the whole root vconcat in a column. A is
    // pinned to d2 (it would otherwise rebind to root); B's old hconcat collapses to a
    // bare unit that keeps d2.
    const next = wrapContainer(spec, [], ['vconcat', 0, 'hconcat', 0], 'horizontal', 'before');
    expect(next).toEqual({
      data: { name: 'root' },
      hconcat: [
        { data: { name: 'd2' }, mark: 'a' },
        { vconcat: [{ data: { name: 'd2' }, mark: 'b' }, { mark: 'c' }] },
      ],
    });
  });

  it('returns null when the source is an ancestor of the container', () => {
    const spec = {
      vconcat: [
        { hconcat: [{ vconcat: [{ mark: 'a' }, { mark: 'b' }] }, { mark: 'c' }] },
        { mark: 'd' },
      ],
    };
    // Drag the outer hconcat onto the inner vconcat's margin — the container is nested
    // inside the source, a degenerate drop.
    expect(
      wrapContainer(spec, ['vconcat', 0, 'hconcat', 0], ['vconcat', 0], 'vertical', 'after'),
    ).toBeNull();
  });
});

describe('simplifyStructure — collapse redundant single-child wrappers', () => {
  it('unwraps a single-child hconcat to the bare view', () => {
    const spec = { data: { name: 'd' }, hconcat: [{ mark: 'bar' }] };
    expect(simplifyStructure(spec)).toEqual({ data: { name: 'd' }, mark: 'bar' });
  });

  it('collapses nested single-child wrappers recursively', () => {
    const spec = { vconcat: [{ hconcat: [{ mark: 'a' }] }, { mark: 'b' }] };
    expect(simplifyStructure(spec)).toEqual({ vconcat: [{ mark: 'a' }, { mark: 'b' }] });
  });

  it('the lone child wins on key conflict, like unwrapSingleton', () => {
    const spec = { width: 100, layer: [{ width: 200, mark: 'line' }] };
    expect(simplifyStructure(spec)).toEqual({ width: 200, mark: 'line' });
  });

  it('returns null when there is nothing to simplify', () => {
    expect(simplifyStructure({ hconcat: [{ mark: 'a' }, { mark: 'b' }] })).toBeNull();
  });

  it('leaves a facet’s single child alone (one child by design)', () => {
    const spec = { facet: { field: 'c', type: 'nominal' }, spec: { mark: 'bar' } };
    expect(simplifyStructure(spec)).toBeNull();
  });
});
