import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { wireEntityWriteThrough } from './entity-persistence';

/**
 * Direct contract test for the shared write-through engine. The four
 * `*-persistence.ts` wrappers exercise it transitively (see
 * snippet-persistence.test.ts), but this is the engine four tiers depend on, so
 * its diff/save/delete/error behavior is pinned here against a minimal store —
 * including the number-id path the (string-id) snippet test never hits.
 */

interface Item {
  id: number;
  v: number;
}
interface State {
  items: Item[];
  other?: number;
}

/** Minimal `StoreLike` the helper accepts, plus a `setState` for the test to drive. */
function makeStore(initial: State) {
  let state = initial;
  const listeners = new Set<(s: State) => void>();
  return {
    getState: () => state,
    setState: (next: State) => {
      state = next;
      listeners.forEach((l) => l(state));
    },
    subscribe: (l: (s: State) => void) => {
      listeners.add(l);
      return () => listeners.delete(l);
    },
  };
}

/** Flush microtasks so a `.catch` on a rejected io promise runs. */
const flush = () => Promise.resolve().then(() => Promise.resolve());

let store: ReturnType<typeof makeStore>;
let save: ReturnType<typeof vi.fn>;
let remove: ReturnType<typeof vi.fn>;
let onError: ReturnType<typeof vi.fn>;
let teardown: () => void;

beforeEach(() => {
  store = makeStore({ items: [{ id: 1, v: 1 }] });
  save = vi.fn().mockResolvedValue(undefined);
  remove = vi.fn().mockResolvedValue(undefined);
  onError = vi.fn();
  teardown = wireEntityWriteThrough(store, (s) => s.items, { save, remove, onError });
});

afterEach(() => teardown());

describe('wireEntityWriteThrough', () => {
  it('saves a newly added record (number id) and leaves untouched refs alone', () => {
    const kept = store.getState().items[0];
    store.setState({ items: [kept, { id: 2, v: 1 }] });

    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith({ id: 2, v: 1 });
    expect(remove).not.toHaveBeenCalled();
  });

  it('saves only the record whose reference changed', () => {
    store.setState({ items: [{ id: 1, v: 2 }] });

    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith({ id: 1, v: 2 });
  });

  it('removes a dropped record by id without re-saving the survivors', () => {
    const one = store.getState().items[0];
    const two = { id: 2, v: 1 };
    store.setState({ items: [one, two] });
    save.mockClear();

    store.setState({ items: [two] }); // drop 1, keep 2's reference

    expect(remove).toHaveBeenCalledTimes(1);
    expect(remove).toHaveBeenCalledWith(1);
    expect(save).not.toHaveBeenCalled();
  });

  it('ignores state changes that do not touch the selected array', () => {
    store.setState({ ...store.getState(), other: 5 });

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

  it('routes a rejected save to onError instead of losing it', async () => {
    save.mockRejectedValueOnce(new Error('boom'));
    const kept = store.getState().items[0];
    store.setState({ items: [kept, { id: 2, v: 1 }] });
    await flush();

    expect(onError).toHaveBeenCalledWith('save', expect.any(Error));
  });

  it('routes a rejected delete to onError', async () => {
    remove.mockRejectedValueOnce(new Error('boom'));
    const two = { id: 2, v: 1 };
    store.setState({ items: [store.getState().items[0], two] });
    store.setState({ items: [two] });
    await flush();

    expect(onError).toHaveBeenCalledWith('delete', expect.any(Error));
  });

  it('detaches on teardown', () => {
    teardown();
    store.setState({ items: [...store.getState().items, { id: 9, v: 1 }] });

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