import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useDebounce } from "./useDebounce.js";

describe("useDebounce", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

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

  it("returns the initial value immediately", () => {
    const { result } = renderHook(() => useDebounce("hello", 300));
    expect(result.current).toBe("hello");
  });

  it("does not update the value before the delay expires", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }: { value: string; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: "initial", delay: 300 } },
    );

    rerender({ value: "updated", delay: 300 });

    act(() => {
      vi.advanceTimersByTime(200);
    });

    expect(result.current).toBe("initial");
  });

  it("updates the value after the delay expires", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }: { value: string; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: "initial", delay: 300 } },
    );

    rerender({ value: "updated", delay: 300 });

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

    expect(result.current).toBe("updated");
  });

  it("resets the timer when the value changes before the delay expires", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }: { value: string; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: "initial", delay: 300 } },
    );

    rerender({ value: "first", delay: 300 });
    act(() => {
      vi.advanceTimersByTime(200);
    });
    // Timer not expired yet — value should still be "initial"
    expect(result.current).toBe("initial");

    rerender({ value: "second", delay: 300 });
    act(() => {
      vi.advanceTimersByTime(200);
    });
    // Only 200ms since the second change — still debouncing
    expect(result.current).toBe("initial");

    act(() => {
      vi.advanceTimersByTime(100);
    });
    // Now 300ms since "second" — should have resolved
    expect(result.current).toBe("second");
  });

  it("works with number values", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }: { value: number; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: 0, delay: 500 } },
    );

    rerender({ value: 42, delay: 500 });

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

    expect(result.current).toBe(42);
  });

  it("works with object values", () => {
    const initial = { name: "Alice" };
    const updated = { name: "Bob" };

    const { result, rerender } = renderHook(
      ({ value, delay }: { value: { name: string }; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: initial, delay: 200 } },
    );

    rerender({ value: updated, delay: 200 });

    act(() => {
      vi.advanceTimersByTime(200);
    });

    expect(result.current).toEqual({ name: "Bob" });
  });

  it("handles a delay of 0", () => {
    const { result, rerender } = renderHook(
      ({ value }: { value: string }) => useDebounce(value, 0),
      { initialProps: { value: "initial" } },
    );

    rerender({ value: "instant" });

    act(() => {
      vi.advanceTimersByTime(0);
    });

    expect(result.current).toBe("instant");
  });

  it("updates the debounced value when delay changes", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }: { value: string; delay: number }) => useDebounce(value, delay),
      { initialProps: { value: "hello", delay: 1000 } },
    );

    // Reduce delay — old timer is cleared, new shorter timer starts
    rerender({ value: "hello", delay: 100 });

    act(() => {
      vi.advanceTimersByTime(100);
    });

    expect(result.current).toBe("hello");
  });
});
