// @vitest-environment jsdom
//
// Covers the Part-1 additions to `@djangocfg/ui-core`'s `useLocalStorage`:
// partial-merge `patch`, no-op skip, write coalescing, and same-tab
// cross-instance sync. ui-core ships no test runner, so the test lives
// here (ui-tools has vitest + jsdom and resolves ui-core via workspace).
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { useLocalStorage } from '@djangocfg/ui-core/hooks';

(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;

interface Prefs {
  a: number;
  b: number;
  c: string;
}

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
  window.localStorage.clear();
  container = document.createElement('div');
  document.body.appendChild(container);
  root = createRoot(container);
});

afterEach(() => {
  act(() => root.unmount());
  container.remove();
  window.localStorage.clear();
});

/** Flush pending microtask-coalesced writes. */
async function flushMicrotasks() {
  await act(async () => {
    await Promise.resolve();
  });
}

describe('useLocalStorage — patch / coalescing / sync', () => {
  it('patch shallow-merges a subset of keys', async () => {
    let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
    function Probe() {
      api = useLocalStorage<Prefs>('t.patch', { a: 1, b: 2, c: 'x' });
      return null;
    }
    await act(async () => root.render(createElement(Probe)));

    await act(async () => {
      api![3]({ b: 99 });
    });
    expect(api![0]).toEqual({ a: 1, b: 99, c: 'x' });

    await flushMicrotasks();
    expect(JSON.parse(window.localStorage.getItem('t.patch')!)).toEqual({
      a: 1,
      b: 99,
      c: 'x',
    });
  });

  it('patch is a no-op when nothing changes', async () => {
    let renders = 0;
    let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
    function Probe() {
      renders += 1;
      api = useLocalStorage<Prefs>('t.noop', { a: 1, b: 2, c: 'x' });
      return null;
    }
    await act(async () => root.render(createElement(Probe)));
    await flushMicrotasks();
    const rendersAfterMount = renders;

    // Patching with the same values must not trigger a write/re-render.
    await act(async () => {
      api![3]({ a: 1, b: 2 });
    });
    await flushMicrotasks();

    expect(renders).toBe(rendersAfterMount);
    // No write happened — key stays absent.
    expect(window.localStorage.getItem('t.noop')).toBeNull();
  });

  it('coalesces several patches in one tick into a single write', async () => {
    let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
    function Probe() {
      api = useLocalStorage<Prefs>('t.coalesce', { a: 0, b: 0, c: '' });
      return null;
    }
    await act(async () => root.render(createElement(Probe)));

    // Spy on the prototype — jsdom's per-instance `setItem` is read-only.
    let writes = 0;
    const realSetItem = Storage.prototype.setItem;
    Storage.prototype.setItem = function patched(this: Storage, k, v) {
      if (k === 't.coalesce') writes += 1;
      return realSetItem.call(this, k, v);
    };

    await act(async () => {
      api![3]({ a: 1 });
      api![3]({ b: 2 });
      api![3]({ c: 'z' });
    });
    await flushMicrotasks();
    Storage.prototype.setItem = realSetItem;

    // State reflects all three patches...
    expect(api![0]).toEqual({ a: 1, b: 2, c: 'z' });
    // ...but they collapsed into exactly one localStorage write.
    expect(writes).toBe(1);
  });

  it('two hook instances on the same key stay in sync (same tab)', async () => {
    let a: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
    let b: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
    function Probe() {
      a = useLocalStorage<Prefs>('t.sync', { a: 1, b: 1, c: 'x' });
      b = useLocalStorage<Prefs>('t.sync', { a: 1, b: 1, c: 'x' });
      return null;
    }
    await act(async () => root.render(createElement(Probe)));

    await act(async () => {
      a![3]({ b: 42 });
    });
    await flushMicrotasks();

    // The sibling instance saw the write without its own setter being called.
    expect(a![0]).toEqual({ a: 1, b: 42, c: 'x' });
    expect(b![0]).toEqual({ a: 1, b: 42, c: 'x' });
  });
});
