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("starts with idle status", () => {
    const save = vi.fn().mockResolvedValue(undefined);
    const { result } = renderHook(() => useAutoSave("initial", save));
    expect(result.current.status).toBe("idle");
    expect(result.current.errorText).toBeNull();
  });

  it("does not save immediately when value changes", () => {
    const save = vi.fn().mockResolvedValue(undefined);
    const { rerender } = renderHook(({ value }) => useAutoSave(value, save), {
      initialProps: { value: "initial" },
    });

    rerender({ value: "changed" });
    expect(save).not.toHaveBeenCalled();
  });

  it("calls save after debounce period", async () => {
    const save = vi.fn().mockResolvedValue(undefined);
    const { rerender } = renderHook(({ value }) => useAutoSave(value, save, { debounceMs: 500 }), {
      initialProps: { value: "initial" },
    });

    rerender({ value: "changed" });
    expect(save).not.toHaveBeenCalled();

    act(() => { vi.advanceTimersByTime(500); });
    await act(() => Promise.resolve());

    expect(save).toHaveBeenCalledWith("changed");
  });

  it("transitions to saving then saved on success", async () => {
    let resolveSave!: () => void;
    const save = vi.fn().mockImplementation(
      () => new Promise<void>((resolve) => { resolveSave = resolve; }),
    );

    const { result, rerender } = renderHook(
      ({ value }) => useAutoSave(value, save, { debounceMs: 100 }),
      { initialProps: { value: "v1" } },
    );

    rerender({ value: "v2" });
    act(() => { vi.advanceTimersByTime(100); });
    // Let the microtask queue run so the save call starts.
    await act(() => Promise.resolve());
    expect(result.current.status).toBe("saving");

    await act(() => { resolveSave(); return Promise.resolve(); });
    expect(result.current.status).toBe("saved");
  });

  it("transitions to error status on save failure", async () => {
    const save = vi.fn().mockRejectedValue(new Error("network error"));

    const { result, rerender } = renderHook(
      ({ value }) => useAutoSave(value, save, { debounceMs: 100 }),
      { initialProps: { value: "v1" } },
    );

    rerender({ value: "v2" });
    act(() => { vi.advanceTimersByTime(100); });
    // Wait for the save promise to reject and state to update.
    await act(() => Promise.resolve());
    await act(() => Promise.resolve());

    expect(result.current.status).toBe("error");
    expect(result.current.errorText).toBe("network error");
  });

  it("retry triggers another save attempt", async () => {
    const save = vi.fn()
      .mockRejectedValueOnce(new Error("first failure"))
      .mockResolvedValue(undefined);

    const { result, rerender } = renderHook(
      ({ value }) => useAutoSave(value, save, { debounceMs: 100 }),
      { initialProps: { value: "v1" } },
    );

    rerender({ value: "v2" });
    act(() => { vi.advanceTimersByTime(100); });
    await act(() => Promise.resolve());
    await act(() => Promise.resolve());
    expect(result.current.status).toBe("error");

    await act(() => { result.current.retry(); return Promise.resolve(); });
    await act(() => Promise.resolve());

    expect(result.current.status).toBe("saved");
    expect(save).toHaveBeenCalledTimes(2);
  });

  it("debounces rapid value changes — calls save only once", async () => {
    const save = vi.fn().mockResolvedValue(undefined);
    const { rerender } = renderHook(
      ({ value }) => useAutoSave(value, save, { debounceMs: 300 }),
      { initialProps: { value: "v1" } },
    );

    rerender({ value: "v2" });
    vi.advanceTimersByTime(100);
    rerender({ value: "v3" });
    vi.advanceTimersByTime(100);
    rerender({ value: "v4" });

    act(() => { vi.advanceTimersByTime(300); });
    await act(() => Promise.resolve());

    expect(save).toHaveBeenCalledTimes(1);
    expect(save).toHaveBeenCalledWith("v4");
  });

  it("clears timers on unmount", () => {
    const save = vi.fn().mockResolvedValue(undefined);
    const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");

    const { rerender, unmount } = renderHook(
      ({ value }) => useAutoSave(value, save, { debounceMs: 500 }),
      { initialProps: { value: "v1" } },
    );

    rerender({ value: "v2" });
    unmount();

    expect(clearTimeoutSpy).toHaveBeenCalled();
    clearTimeoutSpy.mockRestore();
  });
});

