import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

/**
 * Utility function to create debounced functions
 * Used in GenericMediaListPage for search input debouncing
 */
export function createDebouncedFunction<T extends (...args: any[]) => any>(
  fn: T,
  delayMs: number = 300
) {
  let timeout: ReturnType<typeof setTimeout> | null = null;

  return (...args: Parameters<T>) => {
    if (timeout) clearTimeout(timeout);

    timeout = setTimeout(() => {
      fn(...args);
      timeout = null;
    }, delayMs);
  };
}

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

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

  describe("Basic Debouncing", () => {
    it("should delay function execution", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      debouncedFn("test");

      // Should not be called immediately
      expect(mockFn).not.toHaveBeenCalled();

      // Advance time by 300ms
      vi.advanceTimersByTime(300);

      // Now it should be called
      expect(mockFn).toHaveBeenCalledWith("test");
      expect(mockFn).toHaveBeenCalledTimes(1);
    });

    it("should not call function if timer is cleared before delay", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      debouncedFn("test");
      vi.advanceTimersByTime(150);

      // Call again before delay completes
      debouncedFn("updated");

      // First timeout should be cleared
      vi.advanceTimersByTime(150);

      // Should still not have been called
      expect(mockFn).not.toHaveBeenCalled();

      // Complete the second timeout
      vi.advanceTimersByTime(300);

      // Should be called once with latest value
      expect(mockFn).toHaveBeenCalledWith("updated");
      expect(mockFn).toHaveBeenCalledTimes(1);
    });

    it("should handle multiple rapid calls", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      // Rapid calls
      debouncedFn("a");
      vi.advanceTimersByTime(100);

      debouncedFn("b");
      vi.advanceTimersByTime(100);

      debouncedFn("c");
      vi.advanceTimersByTime(100);

      // Should not be called yet
      expect(mockFn).not.toHaveBeenCalled();

      // Complete the final timeout
      vi.advanceTimersByTime(300);

      // Should be called once with the last value
      expect(mockFn).toHaveBeenCalledWith("c");
      expect(mockFn).toHaveBeenCalledTimes(1);
    });

    it("should call multiple times if calls are spaced out", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      debouncedFn("first");
      vi.advanceTimersByTime(300);

      // Should be called
      expect(mockFn).toHaveBeenCalledWith("first");
      expect(mockFn).toHaveBeenCalledTimes(1);

      // Wait enough time and call again
      vi.advanceTimersByTime(200);
      debouncedFn("second");
      vi.advanceTimersByTime(300);

      // Should be called again
      expect(mockFn).toHaveBeenCalledWith("second");
      expect(mockFn).toHaveBeenCalledTimes(2);
    });
  });

  describe("Custom Delay", () => {
    it("should respect custom delay values", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 500);

      debouncedFn("test");

      // 300ms shouldn't trigger
      vi.advanceTimersByTime(300);
      expect(mockFn).not.toHaveBeenCalled();

      // But 500ms should
      vi.advanceTimersByTime(200);
      expect(mockFn).toHaveBeenCalledWith("test");
    });

    it("should handle zero delay", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 0);

      debouncedFn("test");

      vi.advanceTimersByTime(0);

      expect(mockFn).toHaveBeenCalledWith("test");
    });
  });

  describe("Search Use Case", () => {
    it("should debounce search queries correctly", () => {
      const mockSearch = vi.fn();
      const debouncedSearch = createDebouncedFunction(mockSearch, 300);

      // User types "t"
      debouncedSearch("t");
      expect(mockSearch).not.toHaveBeenCalled();

      // User types "te" quickly
      vi.advanceTimersByTime(100);
      debouncedSearch("te");
      expect(mockSearch).not.toHaveBeenCalled();

      // User types "tes"
      vi.advanceTimersByTime(100);
      debouncedSearch("tes");
      expect(mockSearch).not.toHaveBeenCalled();

      // User types "test"
      vi.advanceTimersByTime(100);
      debouncedSearch("test");
      expect(mockSearch).not.toHaveBeenCalled();

      // Wait for debounce delay
      vi.advanceTimersByTime(300);

      // Should only call once with final value
      expect(mockSearch).toHaveBeenCalledWith("test");
      expect(mockSearch).toHaveBeenCalledTimes(1);
    });

    it("should cancel pending search if input clears quickly", () => {
      const mockSearch = vi.fn();
      const debouncedSearch = createDebouncedFunction(mockSearch, 300);

      // User types "test"
      debouncedSearch("test");
      vi.advanceTimersByTime(100);

      // User clears input
      debouncedSearch("");
      vi.advanceTimersByTime(100);

      // User types again
      debouncedSearch("new");
      vi.advanceTimersByTime(300);

      // Should only call with final value
      expect(mockSearch).toHaveBeenCalledWith("new");
      expect(mockSearch).toHaveBeenCalledTimes(1);
    });

    it("should work with async search functions", () => {
      const mockAsyncSearch = vi.fn().mockResolvedValue([]);
      const debouncedSearch = createDebouncedFunction(mockAsyncSearch, 300);

      debouncedSearch("query");
      vi.advanceTimersByTime(300);

      expect(mockAsyncSearch).toHaveBeenCalledWith("query");
    });
  });

  describe("Generic Parameter Handling", () => {
    it("should preserve function parameters", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      const obj = { id: "123", name: "test" };
      debouncedFn("string", 42, obj);

      vi.advanceTimersByTime(300);

      expect(mockFn).toHaveBeenCalledWith("string", 42, obj);
    });

    it("should handle functions with no parameters", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      debouncedFn();

      vi.advanceTimersByTime(300);

      expect(mockFn).toHaveBeenCalledWith();
    });

    it("should handle complex object parameters", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      const options = {
        query: "test",
        filters: { type: "Audio", limit: 100 },
        sort: { by: "SortName", order: "Ascending" },
      };

      debouncedFn(options);

      vi.advanceTimersByTime(300);

      expect(mockFn).toHaveBeenCalledWith(options);
    });
  });

  describe("Memory Management", () => {
    it("should clean up timeout after execution", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 300);

      debouncedFn("test");
      vi.advanceTimersByTime(300);

      expect(mockFn).toHaveBeenCalled();

      const callCount = mockFn.mock.calls.length;

      // Call again shortly after
      debouncedFn("test2");
      vi.advanceTimersByTime(100);

      // Additional calls within delay shouldn't cause multiple executions
      debouncedFn("test3");
      vi.advanceTimersByTime(300);

      // Should only have been called 2 times total
      expect(mockFn.mock.calls.length).toBe(2);
    });

    it("should handle repeated debouncing without memory leaks", () => {
      const mockFn = vi.fn();
      const debouncedFn = createDebouncedFunction(mockFn, 50);

      // Simulate 100 rapid calls
      for (let i = 0; i < 100; i++) {
        debouncedFn(`call${i}`);
        vi.advanceTimersByTime(10);
      }

      // Complete final timeout
      vi.advanceTimersByTime(50);

      // Should only be called once with the last value
      expect(mockFn).toHaveBeenCalledWith("call99");
      expect(mockFn).toHaveBeenCalledTimes(1);
    });
  });
});
