// Tests for persistence.js — the editor-state save/load layer.
//
// What this guards
// ────────────────
// persistence.js is one layer above storage.js. It adds the
// editor-specific knowledge of WHAT shape gets saved and which
// fields shouldn't survive a reload. The crucial logic is:
//
//   1. Round-trip integrity: elements + preferences come back exactly
//      as they went in, modulo the next point.
//   2. Blob URL stripping: elements whose `src` is a `blob:` URL
//      are dropped at save time. Blob URLs go stale immediately on
//      reload, so persisting them produces broken-image elements
//      after refresh.
//
// The stripping is the rule with the most blast radius — if it ever
// regresses, every user who'd uploaded a photo would see broken-image
// placeholders on reload. The tests below pin that behavior precisely
// (including the negative case: data: and https:// URLs must survive).
//
// Test isolation
// ──────────────
// Same pattern as storage.test.js: clear localStorage in beforeEach
// so the missing-state assertions actually see a clean slate.

import {
  loadPersistedState,
  savePersistedState,
  clearPersistedState,
} from './persistence';

// The storage key is private to persistence.js. We don't import it;
// we test through the public API instead. The only time we touch
// localStorage directly is in beforeEach to clear state.

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

  describe('loadPersistedState on empty storage', () => {
    it('returns null when nothing has been saved', () => {
      // Fresh user, first visit. Caller treats null as "start with
      // defaults". If this regressed to returning an empty object,
      // the caller would think there's a saved session and skip
      // the default-template-load path.
      expect(loadPersistedState()).toBeNull();
    });
  });

  describe('savePersistedState / loadPersistedState round-trip', () => {
    it('round-trips a typical editor state', () => {
      const state = {
        elements: [
          {
            id: 'a',
            type: 'image',
            src: 'data:image/png;base64,abc',
            x: 10,
            y: 20,
            width: 100,
            height: 100,
          },
          {
            id: 'b',
            type: 'text',
            text: 'Hello',
            fontSize: 48,
            fontFamily: 'Pacifico',
            x: 50,
            y: 200,
          },
        ],
        activeTemplateId: 'tee-001',
        preferences: { lastColor: '#ec4899', lastSize: 'M' },
      };
      savePersistedState(state);
      expect(loadPersistedState()).toEqual(state);
    });

    it('preserves remote URL src values', () => {
      // Sticker library elements have remote URLs like `/stickers/foo.svg`
      // — these survive reload and must be kept.
      const state = {
        elements: [
          { id: 'a', type: 'sticker', src: '/stickers/star.svg' },
        ],
      };
      savePersistedState(state);
      const loaded = loadPersistedState();
      expect(loaded.elements).toHaveLength(1);
      expect(loaded.elements[0].src).toBe('/stickers/star.svg');
    });

    it('preserves data: URL src values', () => {
      // Cropped photos get saved as data: URLs after the crop apply
      // (they're rasterized from canvas → base64). These survive
      // reload and must be kept.
      const state = {
        elements: [
          { id: 'a', type: 'image', src: 'data:image/png;base64,iVBOR' },
        ],
      };
      savePersistedState(state);
      const loaded = loadPersistedState();
      expect(loaded.elements).toHaveLength(1);
      expect(loaded.elements[0].src).toMatch(/^data:/);
    });
  });

  describe('blob URL stripping at save time', () => {
    it('strips elements whose src is a blob: URL', () => {
      // The headline rule: blob: URLs don't survive reload. The
      // user has uploaded a photo, the editor created an
      // `Object URL` for the file, the URL became `blob:https://...`,
      // and persisting it would result in a 404'd image on reload.
      // The fix is to drop the element entirely so the saved state
      // doesn't contain a broken reference.
      const state = {
        elements: [
          { id: 'a', type: 'image', src: 'blob:https://example.com/abc-123' },
          { id: 'b', type: 'text', text: 'Hello', x: 0, y: 0 },
        ],
      };
      savePersistedState(state);
      const loaded = loadPersistedState();
      expect(loaded.elements).toHaveLength(1);
      expect(loaded.elements[0].id).toBe('b');
    });

    it('keeps multiple non-blob elements alongside the stripped blob element', () => {
      // The filter is per-element. Adjacent good elements must be
      // unaffected even when one is stripped.
      const state = {
        elements: [
          { id: 'a', type: 'image', src: 'data:image/png;base64,abc' },
          { id: 'b', type: 'image', src: 'blob:https://example.com/x' },
          { id: 'c', type: 'sticker', src: '/stickers/star.svg' },
          { id: 'd', type: 'text', text: 'Hi' },
        ],
      };
      savePersistedState(state);
      const loaded = loadPersistedState();
      expect(loaded.elements.map((el) => el.id)).toEqual(['a', 'c', 'd']);
    });

    it('preserves non-element fields when filtering elements', () => {
      // Stripping happens inside the elements array only; the rest
      // of the state object (preferences, activeTemplateId, etc.)
      // is unchanged.
      const state = {
        elements: [
          { id: 'a', type: 'image', src: 'blob:https://example.com/x' },
        ],
        preferences: { lastColor: '#ec4899' },
        activeTemplateId: 'tee-001',
      };
      savePersistedState(state);
      const loaded = loadPersistedState();
      expect(loaded.elements).toEqual([]);
      expect(loaded.preferences).toEqual({ lastColor: '#ec4899' });
      expect(loaded.activeTemplateId).toBe('tee-001');
    });

    it('handles a state with no elements field gracefully', () => {
      // Pre-elements state, or state from a future schema where
      // elements moved elsewhere. The stripping function should
      // not crash — it should just pass the state through.
      const state = { preferences: { lastColor: '#000000' } };
      savePersistedState(state);
      expect(loadPersistedState()).toEqual(state);
    });
  });

  describe('clearPersistedState', () => {
    it('removes saved state so subsequent loads return null', () => {
      savePersistedState({ elements: [{ id: 'a', type: 'text', text: 'Hi' }] });
      expect(loadPersistedState()).not.toBeNull();
      clearPersistedState();
      expect(loadPersistedState()).toBeNull();
    });

    it('is a no-op when nothing was saved', () => {
      // Defensive — calling clear on an empty store shouldn't throw.
      expect(() => clearPersistedState()).not.toThrow();
      expect(loadPersistedState()).toBeNull();
    });
  });

  describe('malformed saved data', () => {
    it('treats a non-object saved value as no saved state', () => {
      // If something other than our save format ends up at the
      // storage key (e.g. an older app version wrote a string), the
      // load path must return null rather than hand back a value
      // the caller can't safely destructure.
      window.localStorage.setItem('pawfectly-yours-state-v1', JSON.stringify('not an object'));
      expect(loadPersistedState()).toBeNull();
    });

    it('treats malformed JSON at the storage key as no saved state', () => {
      // Same protection one layer up — storage.readJson already
      // returns null on JSON.parse failure, persistence inherits
      // that. Pinning the behavior here ensures the contract holds
      // end-to-end through both layers.
      window.localStorage.setItem('pawfectly-yours-state-v1', '{not valid');
      expect(loadPersistedState()).toBeNull();
    });
  });
});
