import { describe, expect, test } from 'vitest';
import {
  CURRENT_SNIPPET_VERSION,
  createSnippet,
  deriveSnippetName,
  duplicateSnippet,
  formatSnippetSize,
  generateSnippetName,
  hasUnpublishedChanges,
  isAutoNamed,
  isDefaultSnippetName,
  SAMPLE_SPEC,
  sampleSpecText,
  snippetSizeBytes,
} from './snippet';

describe('createSnippet', () => {
  test('stamps current version, equal timestamps, and the sample template', () => {
    const now = new Date('2026-06-04T14:30:07.000Z');
    const s = createSnippet({ now, id: 'fixed-id' });

    expect(s.id).toBe('fixed-id');
    expect(s.version).toBe(CURRENT_SNIPPET_VERSION);
    expect(s.created).toBe(now.toISOString());
    expect(s.modified).toBe(s.created);
    expect(s.spec).toBe(sampleSpecText());
    // draft starts identical to published — nothing to publish yet.
    expect(s.draftSpec).toBe(s.spec);
    expect(s.comment).toBe('');
    expect(s.tags).toEqual([]);
    expect(s.datasetRefs).toEqual([]);
    expect(s.meta).toEqual({});
  });

  test('generates a unique id by default', () => {
    const a = createSnippet();
    const b = createSnippet();
    expect(a.id).not.toBe(b.id);
  });

  test('accepts name and spec overrides', () => {
    const s = createSnippet({ name: 'Custom', spec: '{"mark":"point"}' });
    expect(s.name).toBe('Custom');
    expect(s.spec).toBe('{"mark":"point"}');
    expect(s.draftSpec).toBe('{"mark":"point"}');
  });
});

describe('the sample template', () => {
  test('is valid JSON that round-trips', () => {
    expect(JSON.parse(sampleSpecText())).toEqual(SAMPLE_SPEC);
  });

  test('is a bar chart with inline data', () => {
    expect(SAMPLE_SPEC.mark).toBe('bar');
    expect(SAMPLE_SPEC.data.values.length).toBeGreaterThan(0);
  });
});

describe('generateSnippetName', () => {
  test('formats as "Snippet YYYY-MM-DD HH:MM:SS" with zero-padding', () => {
    // Construct via local-time parts so the test is timezone-independent.
    const now = new Date(2026, 0, 5, 9, 7, 3); // 2026-01-05 09:07:03 local
    expect(generateSnippetName(now)).toBe('Snippet 2026-01-05 09:07:03');
  });

  test('differs second-to-second so quick successive creates stay distinct', () => {
    const a = generateSnippetName(new Date(2026, 5, 4, 14, 30, 7));
    const b = generateSnippetName(new Date(2026, 5, 4, 14, 30, 8));
    expect(a).not.toBe(b);
  });
});

describe('isDefaultSnippetName', () => {
  test('recognizes an untouched auto-generated name', () => {
    expect(isDefaultSnippetName(generateSnippetName(new Date(2026, 0, 5, 9, 7, 3)))).toBe(true);
  });

  test('rejects user-chosen names, including near-misses', () => {
    expect(isDefaultSnippetName('My Chart')).toBe(false);
    expect(isDefaultSnippetName('Snippet 2026-01-05')).toBe(false); // no time part
    expect(isDefaultSnippetName('Snippet 2026-01-05 09:07:03 v2')).toBe(false); // user suffix
  });
});

describe('name provenance (nameSource / isAutoNamed)', () => {
  test('createSnippet stamps provenance: default name → auto, explicit name → user', () => {
    expect(createSnippet({ id: 'a' }).nameSource).toBe('auto');
    expect(createSnippet({ id: 'b', name: 'My Chart' }).nameSource).toBe('user');
  });

  test('a generator can pass a derived name that stays in the auto tier', () => {
    const s = createSnippet({ id: 'a', name: 'Bar chart of x by y', nameSource: 'auto' });
    expect(isAutoNamed(s)).toBe(true);
  });

  test('isAutoNamed proves provenance for legacy records (no nameSource)', () => {
    // Records persisted before nameSource existed carry no provenance; only
    // provably app-picked shapes count as auto.
    const derivable = JSON.stringify({
      mark: 'bar',
      encoding: { x: { field: 'Ship Mode' }, y: { aggregate: 'count' } },
    });
    expect(isAutoNamed({ name: 'Snippet 2026-01-05 09:07:03', spec: '{}' })).toBe(true);
    // Name identical to what the app derives from the published spec → auto.
    expect(isAutoNamed({ name: 'Bar chart of count by Ship Mode', spec: derivable })).toBe(true);
    // Anything else is conservatively user-chosen.
    expect(isAutoNamed({ name: 'My Chart', spec: derivable })).toBe(false);
    expect(isAutoNamed({ name: 'Bar chart of count by Ship Mode', spec: '{}' })).toBe(false);
  });

  test('duplicateSnippet carries the source provenance onto the copy', () => {
    const auto = createSnippet({ id: 'a' });
    const user = createSnippet({ id: 'b', name: 'My Chart' });
    expect(duplicateSnippet(auto, { id: 'a2' }).nameSource).toBe('auto');
    expect(duplicateSnippet(user, { id: 'b2' }).nameSource).toBe('user');
  });
});

describe('deriveSnippetName (content-based library names)', () => {
  const spec = (body: object) => JSON.stringify(body);

  test('a spec-level title wins verbatim', () => {
    expect(deriveSnippetName(spec({ title: ' Quarterly revenue ', mark: 'bar' }))).toBe(
      'Quarterly revenue',
    );
  });

  test("title's object and multi-line forms collapse to one line", () => {
    expect(deriveSnippetName(spec({ title: { text: 'Revenue', subtitle: 'FY26' } }))).toBe(
      'Revenue',
    );
    expect(deriveSnippetName(spec({ title: ['Revenue', 'by quarter'] }))).toBe(
      'Revenue by quarter',
    );
    expect(deriveSnippetName(spec({ title: { text: ['Revenue', 'by quarter'] } }))).toBe(
      'Revenue by quarter',
    );
  });

  test('mark + x/y encodings read like the Chart Builder dialect', () => {
    expect(
      deriveSnippetName(
        spec({
          mark: 'bar',
          encoding: {
            x: { field: 'Ship Mode', type: 'nominal' },
            y: { aggregate: 'count' },
          },
        }),
      ),
    ).toBe('Bar chart of count by Ship Mode');
  });

  test('aggregates phrase as "<agg> of <field>" / "unique <field>"', () => {
    expect(
      deriveSnippetName(
        spec({
          mark: 'line',
          encoding: {
            x: { field: 'date', type: 'temporal' },
            y: { aggregate: 'sum', field: 'revenue' },
          },
        }),
      ),
    ).toBe('Line chart of sum of revenue by date');
    expect(
      deriveSnippetName(
        spec({
          mark: 'point',
          encoding: {
            x: { field: 'region' },
            y: { aggregate: 'distinct', field: 'customer' },
          },
        }),
      ),
    ).toBe('Point chart of unique customer by region');
  });

  test('an object mark contributes its type; a single channel still names', () => {
    expect(
      deriveSnippetName(
        spec({
          mark: { type: 'arc', tooltip: true },
          encoding: { theta: { field: 'share', type: 'quantitative' } },
        }),
      ),
    ).toBe('Arc chart of share');
  });

  test('returns null when there is nothing to describe', () => {
    expect(deriveSnippetName('not json {')).toBeNull();
    expect(deriveSnippetName('[]')).toBeNull();
    expect(deriveSnippetName(spec({}))).toBeNull(); // no mark
    expect(deriveSnippetName(spec({ mark: 'bar' }))).toBeNull(); // no encodings
    expect(deriveSnippetName(spec({ mark: 'bar', encoding: { x: { value: 5 } } }))).toBeNull(); // constants aren't names
  });

  test('the sample template derives a sensible name', () => {
    expect(deriveSnippetName(sampleSpecText())).toBe('Bar chart of value by category');
  });
});

describe('duplicateSnippet', () => {
  const source = {
    ...createSnippet({
      id: 'src',
      name: 'My Chart',
      spec: '{"published":1}',
      now: new Date('2026-01-01T00:00:00Z'),
    }),
    draftSpec: '{"draft":1}',
    comment: 'a note',
    tags: ['imported'],
    datasetRefs: ['Sales'],
    meta: { createdWith: 'chart-builder' },
  };

  test('suffixes the name with "(copy)" and takes a new identity + timestamps', () => {
    const now = new Date('2026-03-04T05:06:07Z');
    const copy = duplicateSnippet(source, { id: 'copy', now });

    expect(copy.id).toBe('copy');
    expect(copy.id).not.toBe(source.id);
    expect(copy.name).toBe('My Chart (copy)');
    expect(copy.version).toBe(CURRENT_SNIPPET_VERSION);
    expect(copy.created).toBe(now.toISOString());
    expect(copy.modified).toBe(now.toISOString());
  });

  test('carries over both spec versions, comment, tags, refs, and meta', () => {
    const copy = duplicateSnippet(source);
    expect(copy.spec).toBe('{"published":1}');
    expect(copy.draftSpec).toBe('{"draft":1}');
    expect(copy.comment).toBe('a note');
    expect(copy.tags).toEqual(['imported']);
    expect(copy.datasetRefs).toEqual(['Sales']);
    expect(copy.meta).toEqual({ createdWith: 'chart-builder' });
  });

  test('clones mutable members so the copy shares no references with its source', () => {
    const copy = duplicateSnippet(source);
    expect(copy.tags).not.toBe(source.tags);
    expect(copy.datasetRefs).not.toBe(source.datasetRefs);
    expect(copy.meta).not.toBe(source.meta);
  });

  test('generates a unique id by default', () => {
    expect(duplicateSnippet(source).id).not.toBe(duplicateSnippet(source).id);
  });
});

describe('hasUnpublishedChanges', () => {
  test('false when draft matches published, true once draft diverges', () => {
    const s = createSnippet({ now: new Date('2026-06-04T00:00:00Z') });
    expect(hasUnpublishedChanges(s)).toBe(false);
    expect(hasUnpublishedChanges({ ...s, draftSpec: s.spec + ' ' })).toBe(true);
  });
});

describe('snippetSizeBytes', () => {
  test('measures the draft as UTF-8 byte length, not character count', () => {
    const s = createSnippet({ spec: 'abc' });
    expect(snippetSizeBytes(s)).toBe(3);
    // A 3-character string of multibyte glyphs is more than 3 bytes.
    expect(snippetSizeBytes({ ...s, draftSpec: '€€€' })).toBe(9);
  });

  test('reflects the working draft, not the published spec', () => {
    const s = createSnippet({ spec: 'ab' });
    expect(snippetSizeBytes({ ...s, draftSpec: 'abcdef' })).toBe(6);
  });
});

describe('formatSnippetSize', () => {
  test('omits the size under ~1 KB (spec §02 clutter rule)', () => {
    expect(formatSnippetSize(0)).toBeNull();
    expect(formatSnippetSize(512)).toBeNull();
    expect(formatSnippetSize(1023)).toBeNull();
  });

  test('shows whole KB at and above the threshold', () => {
    expect(formatSnippetSize(1024)).toBe('1 KB');
    expect(formatSnippetSize(2048)).toBe('2 KB');
    expect(formatSnippetSize(1536)).toBe('2 KB'); // rounds to nearest KB
  });

  test('rolls over to MB for large payloads', () => {
    expect(formatSnippetSize(1024 * 1024)).toBe('1 MB');
    expect(formatSnippetSize(3 * 1024 * 1024)).toBe('3 MB');
  });
});
