import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createSnippet, type Snippet } from '@core/snippet';

/**
 * The persistence subscribers are the subtlest glue in the app: a debounced
 * auto-save and a reference-diff write-through that decides what to upsert vs.
 * delete. They run against a mocked IndexedDB adapter so we assert the *wiring*
 * (when each adapter call fires, with what, and that a rejection surfaces) with
 * no real database.
 */

// Mock the adapter's writes. StorageQuotaError comes from the (unmocked) db
// module, so the error-mapping path (instanceof check) behaves as in production.
vi.mock('../infrastructure/snippet-store', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../infrastructure/snippet-store')>();
  return { ...actual, saveSnippet: vi.fn(), deleteSnippet: vi.fn() };
});

import { saveSnippet, deleteSnippet } from '../infrastructure/snippet-store';
import { StorageQuotaError } from '../infrastructure/db';
import { useNotificationStore } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore';
import { AUTOSAVE_DEBOUNCE_MS, wireSnippetPersistence } from './snippet-persistence';

const save = vi.mocked(saveSnippet);
const del = vi.mocked(deleteSnippet);

let teardown: () => void = () => {};

/** Flush pending microtasks (e.g. a `.catch` on a rejected adapter promise). */
const flush = () => Promise.resolve().then(() => Promise.resolve());

function snippetWith(id: string, spec: string): Snippet {
  return createSnippet({ id, spec });
}

beforeEach(() => {
  vi.useFakeTimers();
  save.mockReset().mockResolvedValue(undefined);
  del.mockReset().mockResolvedValue(undefined);
  useNotificationStore.getState().clear();
  useSnippetStore.getState().reset();
});

afterEach(() => {
  teardown();
  teardown = () => {};
  vi.clearAllTimers();
  vi.useRealTimers();
});

describe('wireDraftAutoSave', () => {
  it('persists a settled, valid edit after the debounce', async () => {
    useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]);
    teardown = wireSnippetPersistence();
    save.mockClear();

    useSnippetStore.getState().updateDraft('{"a":2}');
    expect(save).not.toHaveBeenCalled(); // nothing yet — waiting for the pause

    await vi.advanceTimersByTimeAsync(AUTOSAVE_DEBOUNCE_MS);

    expect(save).toHaveBeenCalledTimes(1);
    expect(save.mock.calls[0][0]).toMatchObject({ id: 'a', draftSpec: '{"a":2}' });
  });

  it('does not persist a half-typed, unparseable buffer', async () => {
    useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]);
    teardown = wireSnippetPersistence();
    save.mockClear();

    useSnippetStore.getState().updateDraft('{ "a":');
    await vi.advanceTimersByTimeAsync(AUTOSAVE_DEBOUNCE_MS);

    expect(save).not.toHaveBeenCalled();
  });
});

describe('wireWriteThrough', () => {
  it('persists a newly created snippet', () => {
    useSnippetStore.getState().hydrate([]);
    teardown = wireSnippetPersistence();
    save.mockClear();

    useSnippetStore.getState().createSnippet({ id: 'n', spec: '{}' });

    expect(save).toHaveBeenCalledTimes(1);
    expect(save.mock.calls[0][0]).toMatchObject({ id: 'n' });
    expect(del).not.toHaveBeenCalled();
  });

  it('deletes a removed snippet without re-saving the survivors', () => {
    useSnippetStore.getState().hydrate([snippetWith('a', '{}'), snippetWith('b', '{}')]);
    teardown = wireSnippetPersistence();
    save.mockClear();

    useSnippetStore.getState().removeSnippet('a');

    expect(del).toHaveBeenCalledTimes(1);
    expect(del).toHaveBeenCalledWith('a');
    // 'b' kept its object reference through the filter, so it is not rewritten.
    expect(save).not.toHaveBeenCalled();
  });

  it('ignores state changes that do not touch the snippets array', () => {
    useSnippetStore.getState().hydrate([snippetWith('a', '{}')]);
    teardown = wireSnippetPersistence();
    save.mockClear();

    useSnippetStore.getState().setEditorView('published');

    expect(save).not.toHaveBeenCalled();
    expect(del).not.toHaveBeenCalled();
  });

  it('surfaces a failed save as an error notification instead of losing it silently', async () => {
    useSnippetStore.getState().hydrate([]);
    teardown = wireSnippetPersistence();
    save.mockClear();
    save.mockRejectedValueOnce(new StorageQuotaError());

    useSnippetStore.getState().createSnippet({ id: 'n', spec: '{}' });
    await flush();

    const notes = useNotificationStore.getState().notifications;
    expect(notes).toHaveLength(1);
    expect(notes[0]).toMatchObject({ kind: 'error', title: 'Storage full' });
  });
});
