// Tests for storage.js — the safe localStorage wrapper.
//
// What this guards
// ────────────────
// `storage.js` is the bottom layer of persistence. Every read/write
// of saved design state, user preferences, and recent picks flows
// through these three functions. Their no-throw contract is the
// foundation that lets `persistence.js` not worry about quota
// errors, Safari private mode, SSR, or malformed JSON.
//
// The tests cover:
//   • Round-trip — write → read returns the original value.
//   • Missing key — read returns null.
//   • Malformed JSON in storage — read returns null (no throw).
//   • removeKey — actually deletes; subsequent read returns null.
//   • All three operations return their documented success/failure
//     value rather than throwing.
//
// Test isolation
// ──────────────
// jsdom gives each test file a single shared localStorage. We clear
// it in beforeEach so tests don't bleed state into each other (a
// missing-key assertion in one test would otherwise fail if a
// previous test had set the key).

import { readJson, writeJson, removeKey } from './storage';

describe('storage', () => {
  beforeEach(() => {
    window.localStorage.clear();
  });

  describe('writeJson / readJson round-trip', () => {
    it('writes a value and reads it back', () => {
      const value = { foo: 'bar', count: 42 };
      const ok = writeJson('test-key', value);
      expect(ok).toBe(true);
      expect(readJson('test-key')).toEqual(value);
    });

    it('round-trips arrays', () => {
      const arr = [1, 'two', { three: 3 }];
      writeJson('arr-key', arr);
      expect(readJson('arr-key')).toEqual(arr);
    });

    it('round-trips deeply nested structures', () => {
      // Persistence saves the full editor state including the elements
      // array, each element with its own crop / previousCrop sub-objects.
      // The wrapper must handle arbitrary nesting cleanly.
      const nested = {
        elements: [
          { id: 'a', crop: { sx: 10, sy: 20, sWidth: 100, sHeight: 80 } },
          { id: 'b', previousCrop: { sx: 5, sy: 5, sWidth: 50, sHeight: 50 } },
        ],
        preferences: { font: 'Pacifico', color: '#ec4899' },
      };
      writeJson('nested-key', nested);
      expect(readJson('nested-key')).toEqual(nested);
    });

    it('round-trips primitive values', () => {
      writeJson('num-key', 42);
      expect(readJson('num-key')).toBe(42);
      writeJson('str-key', 'hello');
      expect(readJson('str-key')).toBe('hello');
      writeJson('bool-key', true);
      expect(readJson('bool-key')).toBe(true);
      writeJson('null-key', null);
      // Note: writing null and reading it back gives null. But missing
      // keys also return null. Callers can't distinguish these — by
      // design, since persistence.loadPersistedState treats both
      // as "no saved state" and starts fresh.
      expect(readJson('null-key')).toBe(null);
    });

    it('writing the same key twice overwrites', () => {
      writeJson('overwritten', { v: 1 });
      writeJson('overwritten', { v: 2 });
      expect(readJson('overwritten')).toEqual({ v: 2 });
    });
  });

  describe('readJson on missing data', () => {
    it('returns null for an unset key', () => {
      // No write before read. The wrapper must return null rather
      // than undefined or throw — callers do `if (!parsed) return null;`
      // and expect the null path to handle missing keys.
      expect(readJson('never-written')).toBeNull();
    });

    it('returns null for the empty string stored at a key', () => {
      // Edge case: if something writes an empty string directly via
      // localStorage.setItem, our wrapper's `if (!raw) return null`
      // catches it (empty string is falsy).
      window.localStorage.setItem('empty-string', '');
      expect(readJson('empty-string')).toBeNull();
    });

    it('returns null for malformed JSON (no throw)', () => {
      // Defensive — if storage gets corrupted somehow (manual edit,
      // a crashed half-write from an older version), the read path
      // shouldn't crash the app. Treating malformed JSON as
      // "no saved state" is the same fallback as missing key.
      window.localStorage.setItem('malformed', '{not valid json');
      expect(readJson('malformed')).toBeNull();
    });
  });

  describe('removeKey', () => {
    it('deletes a key so subsequent reads return null', () => {
      writeJson('to-delete', { v: 1 });
      expect(readJson('to-delete')).toEqual({ v: 1 });
      const ok = removeKey('to-delete');
      expect(ok).toBe(true);
      expect(readJson('to-delete')).toBeNull();
    });

    it('returns true even when the key was already absent', () => {
      // localStorage.removeItem is a no-op for missing keys (doesn't
      // throw). The wrapper preserves that — caller gets `true`
      // either way. Useful for "best-effort" cleanup paths.
      expect(removeKey('never-existed')).toBe(true);
    });
  });
});
