import { describe, expect, test } from 'vitest';
import { humanizeBytes, jsonByteSize, summarizeStorage } from './storage-estimate';

describe('summarizeStorage (composition)', () => {
  test('with origin usage: app = usage − snippets − datasets, total = usage', () => {
    const c = summarizeStorage({ usageBytes: 1000, snippetBytes: 100, datasetBytes: 300 });
    expect(c.originMeasured).toBe(true);
    expect(c.totalBytes).toBe(1000);
    expect(c.segments).toEqual([
      { key: 'snippets', label: 'Snippets', bytes: 100 },
      { key: 'datasets', label: 'Datasets', bytes: 300 },
      { key: 'app', label: 'App', bytes: 600 },
    ]);
  });

  test('without origin usage: snippets + datasets only, total is their sum', () => {
    const c = summarizeStorage({ snippetBytes: 100, datasetBytes: 300 });
    expect(c.originMeasured).toBe(false);
    expect(c.totalBytes).toBe(400);
    expect(c.segments.map((s) => s.key)).toEqual(['snippets', 'datasets']);
  });

  test('usage smaller than measured data falls back to snippets+datasets (estimate lag)', () => {
    const c = summarizeStorage({ usageBytes: 50, snippetBytes: 100, datasetBytes: 300 });
    expect(c.originMeasured).toBe(false);
    expect(c.totalBytes).toBe(400);
    expect(c.segments).toHaveLength(2);
  });

  test('usage exactly equal to measured data yields a zero-byte app segment', () => {
    const c = summarizeStorage({ usageBytes: 400, snippetBytes: 100, datasetBytes: 300 });
    expect(c.originMeasured).toBe(true);
    expect(c.segments.find((s) => s.key === 'app')?.bytes).toBe(0);
  });

  test('invalid / negative / non-finite inputs degrade to 0 bytes', () => {
    const c = summarizeStorage({ usageBytes: NaN, snippetBytes: -5, datasetBytes: Infinity });
    expect(c.segments[0].bytes).toBe(0); // snippets
    expect(c.segments[1].bytes).toBe(0); // datasets
    expect(c.originMeasured).toBe(false); // usage NaN → unusable
    expect(c.totalBytes).toBe(0);
  });

  test('empty workspace: every segment is zero, total zero', () => {
    const c = summarizeStorage({ snippetBytes: 0, datasetBytes: 0 });
    expect(c.totalBytes).toBe(0);
    expect(c.segments.every((s) => s.bytes === 0)).toBe(true);
  });
});

describe('jsonByteSize', () => {
  test('measures the UTF-8 byte length of the JSON serialization', () => {
    expect(jsonByteSize({ a: 1 })).toBe(new TextEncoder().encode('{"a":1}').length);
  });

  test('counts multi-byte characters by their UTF-8 size', () => {
    // "é" is 2 UTF-8 bytes; JSON.stringify("é") => the 4-byte string «"é"».
    expect(jsonByteSize('é')).toBe(4);
  });

  test('an unserializable (circular) value degrades to 0', () => {
    const a: Record<string, unknown> = {};
    a.self = a;
    expect(jsonByteSize(a)).toBe(0);
  });
});

describe('humanizeBytes', () => {
  test('zero renders as whole bytes', () => {
    expect(humanizeBytes(0)).toBe('0 B');
  });

  test('small values stay in whole bytes', () => {
    expect(humanizeBytes(1)).toBe('1 B');
    expect(humanizeBytes(512)).toBe('512 B');
    expect(humanizeBytes(1023)).toBe('1023 B');
  });

  test('KB boundary (1024) and within KB', () => {
    expect(humanizeBytes(1024)).toBe('1.0 KB');
    expect(humanizeBytes(1536)).toBe('1.5 KB');
  });

  test('MB boundary and within MB', () => {
    expect(humanizeBytes(1024 * 1024)).toBe('1.0 MB');
    expect(humanizeBytes(5 * 1024 * 1024)).toBe('5.0 MB');
    expect(humanizeBytes(2.5 * 1024 * 1024)).toBe('2.5 MB');
  });

  test('GB boundary', () => {
    expect(humanizeBytes(1024 * 1024 * 1024)).toBe('1.0 GB');
  });

  test('caps at the largest unit (TB)', () => {
    expect(humanizeBytes(1024 ** 4)).toBe('1.0 TB');
    expect(humanizeBytes(1024 ** 5)).toBe('1024.0 TB');
  });

  test('negative and non-finite inputs degrade to 0 B', () => {
    expect(humanizeBytes(-1)).toBe('0 B');
    expect(humanizeBytes(NaN)).toBe('0 B');
    expect(humanizeBytes(Infinity)).toBe('0 B');
  });
});
