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

interface Person {
  name: string;
  age: number;
  score?: number | null;
}

const people: Person[] = [
  { name: "Charlie", age: 30 },
  { name: "Alice", age: 25 },
  { name: "Bob", age: 35 },
];

describe("useTableSort", () => {
  describe("initial state", () => {
    it("returns rows unsorted when no initial options are provided", () => {
      const { result } = renderHook(() => useTableSort(people));
      expect(result.current.sorted).toEqual(people);
      expect(result.current.sortField).toBeNull();
      expect(result.current.sortDir).toBeNull();
    });

    it("applies initialField and initialDir when provided", () => {
      const { result } = renderHook(() =>
        useTableSort(people, { initialField: "name", initialDir: "asc" }),
      );
      expect(result.current.sortField).toBe("name");
      expect(result.current.sortDir).toBe("asc");
      expect(result.current.sorted.map((p) => p.name)).toEqual(["Alice", "Bob", "Charlie"]);
    });

    it("sorts descending when initialDir is desc", () => {
      const { result } = renderHook(() =>
        useTableSort(people, { initialField: "name", initialDir: "desc" }),
      );
      expect(result.current.sorted.map((p) => p.name)).toEqual(["Charlie", "Bob", "Alice"]);
    });
  });

  describe("toggle", () => {
    it("sets a new sort field with asc direction on first toggle", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      });

      expect(result.current.sortField).toBe("name");
      expect(result.current.sortDir).toBe("asc");
    });

    it("cycles asc → desc on the same field", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      });
      expect(result.current.sortDir).toBe("asc");

      act(() => {
        result.current.toggle("name");
      });
      expect(result.current.sortDir).toBe("desc");
    });

    it("cycles desc → null on the same field", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      }); // asc
      act(() => {
        result.current.toggle("name");
      }); // desc
      act(() => {
        result.current.toggle("name");
      }); // null

      expect(result.current.sortField).toBe("name");
      expect(result.current.sortDir).toBeNull();
    });

    it("resets to asc when switching to a different field", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      }); // name asc
      act(() => {
        result.current.toggle("name");
      }); // name desc
      act(() => {
        result.current.toggle("age");
      }); // age asc (new field)

      expect(result.current.sortField).toBe("age");
      expect(result.current.sortDir).toBe("asc");
    });

    it("accepts a custom getValue extractor", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name", (row) => row.name.toLowerCase());
      });

      expect(result.current.sorted.map((p) => p.name)).toEqual(["Alice", "Bob", "Charlie"]);
    });
  });

  describe("reset", () => {
    it("clears sortField and sortDir", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      });
      act(() => {
        result.current.reset();
      });

      expect(result.current.sortField).toBeNull();
      expect(result.current.sortDir).toBeNull();
    });

    it("returns the original row order after reset", () => {
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      });
      act(() => {
        result.current.reset();
      });

      expect(result.current.sorted).toEqual(people);
    });
  });

  describe("sorted output", () => {
    it("sorts strings alphabetically ascending", () => {
      const { result } = renderHook(() => useTableSort(people));
      act(() => {
        result.current.toggle("name");
      });
      expect(result.current.sorted.map((p) => p.name)).toEqual(["Alice", "Bob", "Charlie"]);
    });

    it("sorts strings alphabetically descending", () => {
      const { result } = renderHook(() => useTableSort(people));
      act(() => {
        result.current.toggle("name");
      }); // asc
      act(() => {
        result.current.toggle("name");
      }); // desc
      expect(result.current.sorted.map((p) => p.name)).toEqual(["Charlie", "Bob", "Alice"]);
    });

    it("sorts numbers ascending", () => {
      const { result } = renderHook(() => useTableSort(people));
      act(() => {
        result.current.toggle("age");
      });
      expect(result.current.sorted.map((p) => p.age)).toEqual([25, 30, 35]);
    });

    it("sorts numbers descending", () => {
      const { result } = renderHook(() => useTableSort(people));
      act(() => {
        result.current.toggle("age");
      }); // asc
      act(() => {
        result.current.toggle("age");
      }); // desc
      expect(result.current.sorted.map((p) => p.age)).toEqual([35, 30, 25]);
    });

    it("places nulls last regardless of direction", () => {
      const rows: Person[] = [
        { name: "A", age: 1, score: null },
        { name: "B", age: 2, score: 10 },
        { name: "C", age: 3, score: null },
        { name: "D", age: 4, score: 5 },
      ];
      const { result } = renderHook(() => useTableSort(rows));

      act(() => {
        result.current.toggle("score");
      }); // asc
      expect(result.current.sorted.map((r) => r.score)).toEqual([5, 10, null, null]);

      act(() => {
        result.current.toggle("score");
      }); // desc
      expect(result.current.sorted.map((r) => r.score)).toEqual([10, 5, null, null]);
    });

    it("does not mutate the original rows array", () => {
      const originalOrder = people.map((p) => p.name);
      const { result } = renderHook(() => useTableSort(people));

      act(() => {
        result.current.toggle("name");
      });

      expect(people.map((p) => p.name)).toEqual(originalOrder);
    });

    it("returns original rows when sortDir is null even if sortField is set", () => {
      const { result } = renderHook(() => useTableSort(people));
      // Cycle through to null dir
      act(() => {
        result.current.toggle("name");
      }); // asc
      act(() => {
        result.current.toggle("name");
      }); // desc
      act(() => {
        result.current.toggle("name");
      }); // null
      expect(result.current.sorted).toEqual(people);
    });
  });

  describe("onSortChange callback", () => {
    it("does not call onSortChange on the initial render", () => {
      const onSortChange = vi.fn();
      renderHook(() =>
        useTableSort(people, {
          initialField: "name",
          initialDir: "asc",
          onSortChange,
        }),
      );
      expect(onSortChange).not.toHaveBeenCalled();
    });

    it("calls onSortChange when the sort field changes", () => {
      const onSortChange = vi.fn();
      const { result } = renderHook(() => useTableSort(people, { onSortChange }));

      act(() => {
        result.current.toggle("name");
      });

      expect(onSortChange).toHaveBeenCalledWith("name", "asc");
    });

    it("calls onSortChange with null when reset", () => {
      const onSortChange = vi.fn();
      const { result } = renderHook(() => useTableSort(people, { onSortChange }));

      act(() => {
        result.current.toggle("age");
      });
      act(() => {
        result.current.reset();
      });

      expect(onSortChange).toHaveBeenLastCalledWith(null, null);
    });
  });

  describe("edge cases", () => {
    it("handles an empty rows array", () => {
      const { result } = renderHook(() => useTableSort<Person>([]));
      act(() => {
        result.current.toggle("name");
      });
      expect(result.current.sorted).toEqual([]);
    });

    it("handles a single-element array", () => {
      const { result } = renderHook(() => useTableSort([people[0]!]));
      act(() => {
        result.current.toggle("name");
      });
      expect(result.current.sorted).toEqual([people[0]]);
    });
  });
});
