import { beforeEach, describe, expect, test } from 'vitest';
import { createSnippet } from '@core/snippet';
import { selectActiveSnippet, selectShownText, useSnippetStore } from './SnippetStore';

const store = () => useSnippetStore.getState();
beforeEach(() => store().reset());

describe('hydrate', () => {
  test('selects the newest-modified snippet by default and loads its draft', () => {
    const older = createSnippet({ id: 'old', now: new Date('2026-01-01T00:00:00Z') });
    const newer = createSnippet({ id: 'new', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([older, newer]);

    expect(store().activeSnippetId).toBe('new');
    expect(store().draftText).toBe(newer.draftSpec);
  });

  test('honors an explicit active id, including null for an empty library', () => {
    store().hydrate([], null);
    expect(store().activeSnippetId).toBeNull();
    expect(store().draftText).toBe('');
  });
});

describe('createSnippet', () => {
  test('prepends the new snippet and makes it active with its draft loaded', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a]);

    const id = store().createSnippet({ id: 'b', now: new Date('2026-03-01T00:00:00Z') });

    expect(id).toBe('b');
    expect(store().activeSnippetId).toBe('b');
    expect(store().snippets[0].id).toBe('b');
    expect(store().draftText).toBe(store().snippets[0].draftSpec);
  });
});

describe('selectSnippet', () => {
  test('switches the active snippet and replaces the editor buffer', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', spec: '{"b":2}', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');

    store().selectSnippet('b');
    expect(store().activeSnippetId).toBe('b');
    expect(store().draftText).toBe('{"b":2}');
  });

  test('flushes the outgoing draft so switching mid-edit does not lose valid work', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', spec: '{"b":2}', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');

    store().updateDraft('{"a":99}'); // edit a, before the auto-save debounce fires
    store().selectSnippet('b'); // switch away immediately

    // The edit lands in the DRAFT (auto-save never touches the published spec).
    const savedA = store().snippets.find((s) => s.id === 'a')!;
    expect(savedA.draftSpec).toBe('{"a":99}');
    expect(savedA.spec).toBe('{"a":1}'); // published untouched until publish
    expect(store().draftText).toBe('{"b":2}');
  });

  test('an unparseable outgoing buffer is left uncommitted on switch', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', spec: '{"b":2}', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');

    store().updateDraft('{"a":'); // half-typed
    store().selectSnippet('b');

    expect(store().snippets.find((s) => s.id === 'a')!.spec).toBe('{"a":1}'); // untouched
  });
});

describe('removeSnippet', () => {
  test('removing the active snippet falls back to the newest remaining one', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', now: new Date('2026-02-01T00:00:00Z') });
    const c = createSnippet({ id: 'c', now: new Date('2026-03-01T00:00:00Z') });
    store().hydrate([a, b, c], 'b');

    store().removeSnippet('b');
    expect(store().snippets.map((s) => s.id)).toEqual(['a', 'c']);
    expect(store().activeSnippetId).toBe('c'); // newest remaining
  });

  test('removing a non-active snippet leaves the selection untouched', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');

    store().removeSnippet('b');
    expect(store().activeSnippetId).toBe('a');
  });

  test('removing the last snippet clears the active id and buffer', () => {
    const a = createSnippet({ id: 'a' });
    store().hydrate([a], 'a');

    store().removeSnippet('a');
    expect(store().activeSnippetId).toBeNull();
    expect(store().draftText).toBe('');
  });
});

describe('updateDraft + commitDraft (auto-save)', () => {
  test('commit writes a valid buffer into the DRAFT only and bumps modified', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":2}');
    expect(store().snippets[0].draftSpec).toBe('{"a":1}'); // buffer-only until commit

    const committed = store().commitDraft(new Date('2026-05-01T00:00:00Z'));
    expect(committed).toBe(true);
    const s = selectActiveSnippet(store())!;
    expect(s.draftSpec).toBe('{"a":2}');
    expect(s.spec).toBe('{"a":1}'); // published spec is NEVER touched by auto-save
    expect(s.modified).toBe('2026-05-01T00:00:00.000Z');
  });

  test('commit is skipped while the buffer is not valid JSON', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}' });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":'); // half-typed
    expect(store().commitDraft()).toBe(false);
    expect(store().snippets[0].draftSpec).toBe('{"a":1}'); // stored draft untouched
  });

  test('commit is a no-op (false) when the draft is unchanged', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}' });
    store().hydrate([a], 'a');
    expect(store().commitDraft()).toBe(false);
  });

  test('commit does nothing when no snippet is active', () => {
    store().hydrate([], null);
    store().updateDraft('{"a":1}');
    expect(store().commitDraft()).toBe(false);
  });
});

describe('publish (spec §03D → Publish)', () => {
  test('promotes the draft to the published spec (the two become identical)', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":2}'); // edit, still uncommitted in the buffer
    const ok = store().publish(new Date('2026-05-01T00:00:00Z'));

    expect(ok).toBe(true);
    const s = selectActiveSnippet(store())!;
    expect(s.spec).toBe('{"a":2}'); // buffer was flushed and promoted
    expect(s.draftSpec).toBe('{"a":2}');
    expect(s.modified).toBe('2026-05-01T00:00:00.000Z');
  });

  test('an invalid live buffer publishes the last valid draft instead', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}' });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":2}');
    store().commitDraft(); // last valid draft = {"a":2}
    store().updateDraft('{"a":'); // now the buffer is half-typed
    store().publish();

    expect(selectActiveSnippet(store())!.spec).toBe('{"a":2}');
  });

  test('returns false when no snippet is active', () => {
    store().hydrate([], null);
    expect(store().publish()).toBe(false);
  });
});

describe('revert (spec §03D → Revert)', () => {
  test('discards draft changes and reloads the editor on the draft view', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}' });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":2}');
    store().commitDraft();
    store().setEditorView('published');

    const ok = store().revert(new Date('2026-05-01T00:00:00Z'));
    expect(ok).toBe(true);
    const s = selectActiveSnippet(store())!;
    expect(s.draftSpec).toBe('{"a":1}'); // restored to published
    expect(s.spec).toBe('{"a":1}');
    expect(store().draftText).toBe('{"a":1}'); // editor reloaded
    expect(store().editorView).toBe('draft'); // back on the editable view
  });

  test('returns false when no snippet is active', () => {
    store().hydrate([], null);
    expect(store().revert()).toBe(false);
  });
});

describe('editorView + selectShownText (spec §03D)', () => {
  test('draft view shows the live buffer; published view shows the stored spec', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}' });
    store().hydrate([a], 'a');
    store().updateDraft('{"a":2}'); // diverge the draft from published

    expect(store().editorView).toBe('draft');
    expect(selectShownText(store())).toBe('{"a":2}');

    store().setEditorView('published');
    expect(selectShownText(store())).toBe('{"a":1}');
    expect(store().draftText).toBe('{"a":2}'); // buffer preserved behind the published view
  });

  test('selecting a different snippet resets the view to draft', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', spec: '{"b":2}', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');
    store().setEditorView('published');

    store().selectSnippet('b');
    expect(store().editorView).toBe('draft');
  });
});

describe('publish — content-derived naming (untouched default names only)', () => {
  const namableSpec = JSON.stringify({
    mark: 'bar',
    encoding: { x: { field: 'Ship Mode' }, y: { aggregate: 'count' } },
  });

  test('publishing upgrades an untouched timestamp name to a content-derived one', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft(namableSpec);
    store().publish(new Date('2026-02-01T00:00:00Z'));

    expect(selectActiveSnippet(store())?.name).toBe('Bar chart of count by Ship Mode');
  });

  test('a user-chosen name is never overwritten', () => {
    const a = createSnippet({ id: 'a', name: 'My Chart' });
    store().hydrate([a], 'a');

    store().updateDraft(namableSpec);
    store().publish();

    expect(selectActiveSnippet(store())?.name).toBe('My Chart');
  });

  test('an undescribable spec keeps the default name', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":2}');
    store().publish();

    expect(selectActiveSnippet(store())?.name).toBe(a.name);
  });

  test('an auto name keeps tracking the spec across publishes', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft(namableSpec);
    store().publish();
    expect(selectActiveSnippet(store())?.name).toBe('Bar chart of count by Ship Mode');

    store().updateDraft(
      JSON.stringify({
        mark: 'line',
        encoding: { x: { field: 'date' }, y: { aggregate: 'sum', field: 'revenue' } },
      }),
    );
    store().publish();
    expect(selectActiveSnippet(store())?.name).toBe('Line chart of sum of revenue by date');
  });

  test('a legacy record whose name matches its own derivation stays in the auto tier', () => {
    // Records written before nameSource existed: a name identical to what the
    // app derives from the published spec is provably app-picked, so adding a
    // title and publishing must adopt it (not stay frozen).
    const legacy = {
      ...createSnippet({ id: 'a', spec: namableSpec, now: new Date('2026-01-01T00:00:00Z') }),
      name: 'Bar chart of count by Ship Mode',
      nameSource: undefined,
    };
    store().hydrate([legacy], 'a');

    store().updateDraft(
      JSON.stringify({
        title: 'Shipments by mode',
        mark: 'bar',
        encoding: { x: { field: 'Ship Mode' }, y: { aggregate: 'count' } },
      }),
    );
    store().publish();

    expect(selectActiveSnippet(store())?.name).toBe('Shipments by mode');
  });

  test('an explicit rename freezes the name against later publishes', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft(namableSpec);
    store().publish();
    store().renameSnippet('a', 'My Chart');

    store().updateDraft(JSON.stringify({ title: 'Something else', mark: 'bar' }));
    store().publish();

    expect(selectActiveSnippet(store())?.name).toBe('My Chart');
  });
});

describe('publish — datasetRefs recomputation', () => {
  const refSpec = (name: string) => JSON.stringify({ data: { name }, mark: 'bar' });

  test('publishing recomputes datasetRefs from the now-published spec', () => {
    const a = createSnippet({ id: 'a', spec: '{}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    store().updateDraft(refSpec('Sales'));
    store().publish(new Date('2026-02-01T00:00:00Z'));

    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Sales']);
  });

  test('refs drop when a published spec no longer references a dataset', () => {
    const a = createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    store().hydrate([a], 'a');
    // The factory leaves datasetRefs empty until a publish runs; publish to seed it.
    store().publish(new Date('2026-01-02T00:00:00Z'));
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Sales']);

    store().updateDraft('{"mark":"bar"}');
    store().publish(new Date('2026-02-01T00:00:00Z'));
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual([]);
  });
});

describe('datasetRefs track the live draft (spec §03F, arch 07 §3)', () => {
  const refSpec = (name: string) => JSON.stringify({ data: { name }, mark: 'bar' }, null, 2);
  const inlineSpec = JSON.stringify({ data: { values: [{ a: 1 }] }, mark: 'bar' }, null, 2);

  test('createSnippet links a named-data spec immediately (Chart Builder path)', () => {
    store().createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Sales']);
  });

  test('auto-save links a hand-typed reference without publishing', () => {
    const a = createSnippet({ id: 'a', spec: '{}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft(refSpec('Sales'));
    store().commitDraft(new Date('2026-02-01T00:00:00Z')); // the debounced auto-save

    const s = selectActiveSnippet(store())!;
    expect(s.datasetRefs).toEqual(['Sales']); // linked before any publish
    expect(s.spec).toBe('{}'); // published spec untouched — refs mirror the draft
  });

  test('replaceActiveDraft (Extract to Dataset) links the dataset immediately', () => {
    const a = createSnippet({ id: 'a', spec: inlineSpec, now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual([]); // inline data → no refs

    store().replaceActiveDraft(refSpec('Orders'), new Date('2026-02-01T00:00:00Z'));

    const s = selectActiveSnippet(store())!;
    expect(s.datasetRefs).toEqual(['Orders']); // linked the moment the draft is rewritten
    expect(s.spec).toBe(inlineSpec); // still a draft edit — published unchanged until publish
  });

  test('revert drops a link that existed only in the discarded draft', () => {
    const a = createSnippet({ id: 'a', spec: inlineSpec, now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    store().replaceActiveDraft(refSpec('Orders'), new Date('2026-02-01T00:00:00Z'));
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Orders']);

    store().revert(new Date('2026-03-01T00:00:00Z'));

    const s = selectActiveSnippet(store())!;
    expect(s.datasetRefs).toEqual([]); // draft restored to the inline-data published spec
    expect(s.draftSpec).toBe(inlineSpec);
  });

  test('an unparseable draft is skipped — refs keep their last valid value (no flicker)', () => {
    const a = createSnippet({ id: 'a', spec: '{}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    store().updateDraft(refSpec('Sales'));
    store().commitDraft(new Date('2026-01-02T00:00:00Z'));
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Sales']);

    store().updateDraft('{ "data": { "name": "Sa'); // mid-keystroke, invalid JSON
    expect(store().commitDraft(new Date('2026-01-03T00:00:00Z'))).toBe(false);
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual(['Sales']); // unchanged
  });
});

describe('renameSnippet (spec §02 metadata panel)', () => {
  test('renames and advances modified, without touching the editor buffer', () => {
    const a = createSnippet({ id: 'a', name: 'Old', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    const epochBefore = store().bufferEpoch;

    store().renameSnippet('a', 'New', new Date('2026-05-01T00:00:00Z'));
    const saved = store().snippets.find((s) => s.id === 'a')!;
    expect(saved.name).toBe('New');
    expect(saved.modified).toBe('2026-05-01T00:00:00.000Z');
    expect(store().bufferEpoch).toBe(epochBefore); // name is not in the spec buffer
  });

  test('is a no-op for an unchanged name (modified does not advance)', () => {
    const a = createSnippet({ id: 'a', name: 'Same', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().renameSnippet('a', 'Same', new Date('2026-05-01T00:00:00Z'));
    expect(store().snippets.find((s) => s.id === 'a')!.modified).toBe('2026-01-01T00:00:00.000Z');
  });

  test('ignores an unknown id', () => {
    const a = createSnippet({ id: 'a' });
    store().hydrate([a], 'a');
    expect(() => store().renameSnippet('missing', 'X')).not.toThrow();
    expect(store().snippets).toHaveLength(1);
  });
});

describe('setComment (spec §02 metadata panel)', () => {
  test('sets the comment and advances modified', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().setComment('a', 'a useful note', new Date('2026-05-02T00:00:00Z'));
    const saved = store().snippets.find((s) => s.id === 'a')!;
    expect(saved.comment).toBe('a useful note');
    expect(saved.modified).toBe('2026-05-02T00:00:00.000Z');
  });

  test('is a no-op for an unchanged comment', () => {
    const a = {
      ...createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') }),
      comment: 'x',
    };
    store().hydrate([a], 'a');

    store().setComment('a', 'x', new Date('2026-05-02T00:00:00Z'));
    expect(store().snippets.find((s) => s.id === 'a')!.modified).toBe('2026-01-01T00:00:00.000Z');
  });
});

describe('duplicateActiveSnippet (spec §02 → Duplicate)', () => {
  test('prepends an independent copy and makes it active', () => {
    const a = {
      ...createSnippet({
        id: 'a',
        name: 'Chart',
        spec: '{"a":1}',
        now: new Date('2026-01-01T00:00:00Z'),
      }),
      comment: 'note',
      tags: ['imported'],
      datasetRefs: ['Sales'],
    };
    store().hydrate([a], 'a');

    const id = store().duplicateActiveSnippet(new Date('2026-04-01T00:00:00Z'), 'copy');
    expect(id).toBe('copy');
    expect(store().activeSnippetId).toBe('copy');
    expect(store().snippets[0].id).toBe('copy'); // prepended

    const copy = store().snippets[0];
    expect(copy.name).toBe('Chart (copy)');
    expect(copy.spec).toBe('{"a":1}');
    expect(copy.comment).toBe('note');
    expect(copy.tags).toEqual(['imported']);
    expect(copy.datasetRefs).toEqual(['Sales']);
    expect(copy.created).toBe('2026-04-01T00:00:00.000Z');
    expect(store().draftText).toBe(copy.draftSpec);
  });

  test('captures in-progress buffer edits before copying', () => {
    const a = createSnippet({ id: 'a', spec: '{"a":1}', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');

    store().updateDraft('{"a":99}'); // edited, not yet auto-saved
    store().duplicateActiveSnippet(new Date('2026-04-01T00:00:00Z'), 'copy');
    expect(store().snippets[0].draftSpec).toBe('{"a":99}'); // copy reflects the live edit
  });

  test('returns null when no snippet is active', () => {
    store().hydrate([], null);
    expect(store().duplicateActiveSnippet()).toBeNull();
  });
});

describe('renameDatasetRefs', () => {
  const refSpec = (name: string) => JSON.stringify({ data: { name }, mark: 'bar' }, null, 2);

  test('rewrites spec, draftSpec, and refs of referencing snippets only', () => {
    const a = createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    const b = createSnippet({
      id: 'b',
      spec: '{"mark":"line"}',
      now: new Date('2026-01-02T00:00:00Z'),
    });
    store().hydrate([a, b], 'b');
    store().publish(new Date('2026-01-03T00:00:00Z')); // seed a's refs would need a active; seed via select
    store().selectSnippet('a');
    store().publish(new Date('2026-01-04T00:00:00Z'));

    const updated = store().renameDatasetRefs('Sales', 'Revenue', new Date('2026-02-01T00:00:00Z'));

    expect(updated).toBe(1);
    const renamed = store().snippets.find((s) => s.id === 'a')!;
    expect(renamed.datasetRefs).toEqual(['Revenue']);
    expect(renamed.spec).toContain('"Revenue"');
    expect(renamed.draftSpec).toContain('"Revenue"');
    // The non-referencing snippet is untouched.
    expect(store().snippets.find((s) => s.id === 'b')!.spec).toBe('{"mark":"line"}');
  });

  test('refreshes the active draft buffer when the active snippet is rewritten', () => {
    const a = createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    store().hydrate([a], 'a');
    store().publish(new Date('2026-01-02T00:00:00Z'));
    const epochBefore = store().bufferEpoch;

    store().renameDatasetRefs('Sales', 'Revenue', new Date('2026-02-01T00:00:00Z'));

    expect(store().draftText).toContain('"Revenue"');
    expect(store().bufferEpoch).toBe(epochBefore + 1);
  });

  test('no-ops when the names are equal or nothing references the old name', () => {
    const a = createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    store().hydrate([a], 'a');
    store().publish(new Date('2026-01-02T00:00:00Z'));

    expect(store().renameDatasetRefs('Sales', 'Sales')).toBe(0);
    expect(store().renameDatasetRefs('Unknown', 'Other')).toBe(0);
  });

  test('rewrites a name referenced only by the published spec (draft removed it)', () => {
    const a = createSnippet({
      id: 'a',
      spec: refSpec('Sales'),
      now: new Date('2026-01-01T00:00:00Z'),
    });
    store().hydrate([a], 'a');
    store().publish(new Date('2026-01-02T00:00:00Z')); // spec + draft reference Sales
    // Remove the reference from the DRAFT only — do not publish. datasetRefs now
    // mirrors the (empty) draft, but the published spec still references Sales.
    store().updateDraft('{"mark":"bar"}');
    store().commitDraft(new Date('2026-01-03T00:00:00Z'));
    expect(selectActiveSnippet(store())?.datasetRefs).toEqual([]);

    const updated = store().renameDatasetRefs('Sales', 'Revenue', new Date('2026-02-01T00:00:00Z'));

    expect(updated).toBe(1); // matched via the published spec, not datasetRefs
    const renamed = store().snippets.find((s) => s.id === 'a')!;
    expect(renamed.spec).toContain('"Revenue"'); // published spec rewritten — no rot
    expect(renamed.spec).not.toContain('"Sales"');
  });
});

describe('library view state (spec §02 → Search / Sort)', () => {
  test('setSearch updates the query without touching the active snippet', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    const b = createSnippet({ id: 'b', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b], 'a');

    store().setSearch('bar');
    expect(store().searchQuery).toBe('bar');
    expect(store().activeSnippetId).toBe('a'); // search is visibility-only
  });

  test('defaults to Modified, descending', () => {
    expect(store().sortBy).toBe('modified');
    expect(store().sortOrder).toBe('desc');
  });

  test('re-selecting the active field flips direction', () => {
    expect(store().sortOrder).toBe('desc');
    store().setSort('modified');
    expect(store().sortBy).toBe('modified');
    expect(store().sortOrder).toBe('asc');
    store().setSort('modified');
    expect(store().sortOrder).toBe('desc');
  });

  test('selecting a different field switches to it and resets to descending', () => {
    store().setSort('name'); // from modified/desc → name/desc
    expect(store().sortBy).toBe('name');
    expect(store().sortOrder).toBe('desc');

    store().setSort('name'); // flip to asc
    expect(store().sortOrder).toBe('asc');

    store().setSort('size'); // different field resets to desc, not carrying asc
    expect(store().sortBy).toBe('size');
    expect(store().sortOrder).toBe('desc');
  });

  test('an explicit order hydrates the exact stored state (no flip logic)', () => {
    store().setSort('name', 'asc');
    expect(store().sortBy).toBe('name');
    expect(store().sortOrder).toBe('asc');
  });

  test('sort changes never touch the active snippet', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a], 'a');
    store().setSort('name');
    expect(store().activeSnippetId).toBe('a');
  });
});

describe('replaceSnippetSpec (Chart Builder edit-in-place save)', () => {
  const newSpec = JSON.stringify(
    {
      $schema: 'https://vega.github.io/schema/vega-lite/v6.json',
      data: { name: 'D' },
      mark: { type: 'point', tooltip: true },
      encoding: { x: { field: 'a', type: 'quantitative' } },
    },
    null,
    2,
  );

  test('republishes both versions, activates the snippet, and bumps modified', () => {
    const a = createSnippet({
      id: 'a',
      spec: '{"mark":"bar"}',
      now: new Date('2026-01-01T00:00:00Z'),
    });
    const b = createSnippet({ id: 'b', now: new Date('2026-02-01T00:00:00Z') });
    store().hydrate([a, b]); // active = b (newest)

    const ok = store().replaceSnippetSpec('a', newSpec, new Date('2026-03-01T00:00:00Z'));
    expect(ok).toBe(true);

    const after = store().snippets.find((s) => s.id === 'a')!;
    expect(after.spec).toBe(newSpec);
    expect(after.draftSpec).toBe(newSpec); // republished — no pending draft
    expect(after.modified).toBe('2026-03-01T00:00:00.000Z');
    expect(after.datasetRefs).toEqual(['D']); // recomputed from the new spec
    expect(store().activeSnippetId).toBe('a'); // the edited snippet becomes active
    expect(store().draftText).toBe(newSpec);
    expect(store().editorView).toBe('draft');
  });

  test('keeps a user-chosen name; returns false for an unknown id', () => {
    const a = createSnippet({ id: 'a', now: new Date('2026-01-01T00:00:00Z') });
    store().hydrate([a]);
    store().renameSnippet('a', 'My chart'); // nameSource -> 'user'

    store().replaceSnippetSpec('a', newSpec);
    expect(store().snippets.find((s) => s.id === 'a')!.name).toBe('My chart');

    expect(store().replaceSnippetSpec('missing', newSpec)).toBe(false);
  });
});
