// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAutoSave } from './useAutoSave';

describe('useAutoSave', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it('debounces: only the latest payload is saved, once', async () => {
    const save = vi.fn().mockResolvedValue();
    const { result } = renderHook(() => useAutoSave(save, 400));

    act(() => {
      result.current.schedule({ a: 1 });
      result.current.schedule({ a: 2 });
      result.current.schedule({ a: 3 });
    });
    expect(save).not.toHaveBeenCalled();

    await act(async () => { await vi.advanceTimersByTimeAsync(400); });
    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith({ a: 3 });
    expect(result.current.status).toBe('saved');
  });

  it('per-call delay overrides the default', async () => {
    const save = vi.fn().mockResolvedValue();
    const { result } = renderHook(() => useAutoSave(save, 400));

    act(() => { result.current.schedule({ slow: true }, 900); });
    await act(async () => { await vi.advanceTimersByTimeAsync(400); });
    expect(save).not.toHaveBeenCalled();
    await act(async () => { await vi.advanceTimersByTimeAsync(500); });
    expect(save).toHaveBeenCalledWith({ slow: true });
  });

  it('flush() saves a pending payload immediately and is a no-op when idle', async () => {
    const save = vi.fn().mockResolvedValue();
    const { result } = renderHook(() => useAutoSave(save, 400));

    await act(async () => { result.current.flush(); });
    expect(save).not.toHaveBeenCalled();

    act(() => { result.current.schedule({ b: 1 }, 900); });
    await act(async () => { result.current.flush(); });
    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith({ b: 1 });

    // payload no longer pending — flushing again must not double-save
    await act(async () => { result.current.flush(); });
    expect(save).toHaveBeenCalledTimes(1);
  });

  it('reports error status when the save rejects, then recovers on next save', async () => {
    const save = vi.fn()
      .mockRejectedValueOnce(new Error('boom'))
      .mockResolvedValueOnce();
    const { result } = renderHook(() => useAutoSave(save, 100));

    act(() => { result.current.schedule({ x: 1 }); });
    await act(async () => { await vi.advanceTimersByTimeAsync(100); });
    expect(result.current.status).toBe('error');

    act(() => { result.current.schedule({ x: 2 }); });
    await act(async () => { await vi.advanceTimersByTimeAsync(100); });
    expect(result.current.status).toBe('saved');
  });

  it('"saved" fades back to idle after 2 seconds', async () => {
    const save = vi.fn().mockResolvedValue();
    const { result } = renderHook(() => useAutoSave(save, 100));

    act(() => { result.current.schedule({ y: 1 }); });
    await act(async () => { await vi.advanceTimersByTimeAsync(100); });
    expect(result.current.status).toBe('saved');

    await act(async () => { await vi.advanceTimersByTimeAsync(2000); });
    expect(result.current.status).toBe('idle');
  });

  it('flushes a pending payload on unmount so edits are never lost', async () => {
    const save = vi.fn().mockResolvedValue();
    const { result, unmount } = renderHook(() => useAutoSave(save, 900));

    act(() => { result.current.schedule({ unsaved: true }); });
    expect(save).not.toHaveBeenCalled();

    unmount();
    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith({ unsaved: true });
  });
});
