// Tests for blobUrl.js — the isBlobUrl predicate and revokeBlobUrl
// wrapper used by persistence stripping and the photo-edit cleanup
// path.
//
// What this guards
// ────────────────
// isBlobUrl is consulted in two places that both have correctness
// consequences:
//
//   1. `persistence.savePersistedState` strips elements whose `src`
//      is a blob: URL before saving to localStorage. Blob URLs go
//      stale on reload — keeping them would produce broken-image
//      elements on next session restore. If isBlobUrl returns false
//      for a real blob URL, those broken images appear after refresh.
//
//   2. The photo-edit flow revokes the previous blob URL when the
//      user applies an edit so the browser frees the underlying
//      memory. If isBlobUrl returns true for a non-blob URL, we'd
//      try to revoke a regular https:// URL, which is at best a no-op
//      and at worst a deopt the browser logs.
//
// Both call sites benefit from a stable predicate, so the tests pin
// the exact behavior: string-typed AND starts with literal "blob:".

import { isBlobUrl, revokeBlobUrl } from './blobUrl';

describe('isBlobUrl', () => {
  it('returns true for a string starting with "blob:"', () => {
    // Real blob URLs look like blob:https://example.com/<uuid>. The
    // predicate only checks the prefix, not the suffix shape — any
    // string starting with "blob:" matches.
    expect(isBlobUrl('blob:https://example.com/abc-123')).toBe(true);
    expect(isBlobUrl('blob:')).toBe(true);
  });

  it('returns false for non-blob URLs', () => {
    // These are the URL shapes we DON'T want to revoke:
    //   - data: URLs (survive a reload, persist fine)
    //   - https:// URLs (CDN-hosted images)
    //   - file:// URLs (local file pickers)
    //   - relative paths
    expect(isBlobUrl('data:image/png;base64,iVBORw0KG')).toBe(false);
    expect(isBlobUrl('https://example.com/photo.jpg')).toBe(false);
    expect(isBlobUrl('file:///Users/khalid/photo.jpg')).toBe(false);
    expect(isBlobUrl('/uploads/photo.jpg')).toBe(false);
    expect(isBlobUrl('photo.jpg')).toBe(false);
  });

  it('returns false for non-strings (no throw)', () => {
    // The docblock specifies: "Returns false for non-strings rather
    // than throwing — callers usually test optional fields like
    // `element.src` and shouldn't have to null-check twice." This
    // is the regression test for that contract.
    expect(isBlobUrl(null)).toBe(false);
    expect(isBlobUrl(undefined)).toBe(false);
    expect(isBlobUrl(42)).toBe(false);
    expect(isBlobUrl({})).toBe(false);
    expect(isBlobUrl([])).toBe(false);
  });

  it('returns false for the empty string', () => {
    expect(isBlobUrl('')).toBe(false);
  });

  it('is case-sensitive ("BLOB:" is not "blob:")', () => {
    // Real browser-issued blob URLs are always lowercase, so this
    // is the desirable behavior. Pinning it here so a future
    // "normalize case" refactor doesn't silently expand the
    // predicate's surface to include uppercase variants we never
    // intended to support.
    expect(isBlobUrl('BLOB:https://example.com/abc')).toBe(false);
    expect(isBlobUrl('Blob:https://example.com/abc')).toBe(false);
  });
});

describe('revokeBlobUrl', () => {
  // jsdom does not provide URL.revokeObjectURL by default. The
  // function under test wraps the call in try/catch precisely to
  // handle environments where it's missing (or where the URL has
  // already been revoked). These tests verify the no-throw contract
  // across the various inputs callers actually send.

  it('does not throw for a blob: URL even if revokeObjectURL is missing', () => {
    // The cleanup path in the photo editor will call this on every
    // edit. It MUST NOT throw — that would interrupt the edit flow
    // and leave the user staring at a half-applied state.
    expect(() => revokeBlobUrl('blob:https://example.com/abc')).not.toThrow();
  });

  it('does not throw for non-blob URLs (no-op)', () => {
    // Defensive: if a caller accidentally passes a data: URL, we
    // shouldn't try to revoke it. The isBlobUrl guard inside
    // revokeBlobUrl short-circuits this case.
    expect(() => revokeBlobUrl('data:image/png;base64,abc')).not.toThrow();
    expect(() => revokeBlobUrl('https://example.com/photo.jpg')).not.toThrow();
  });

  it('does not throw for non-string inputs', () => {
    expect(() => revokeBlobUrl(null)).not.toThrow();
    expect(() => revokeBlobUrl(undefined)).not.toThrow();
    expect(() => revokeBlobUrl(42)).not.toThrow();
  });
});
