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

// ---------------------------------------------------------------------------
// Mock the tRPC client — must be registered before the module is imported.
// ---------------------------------------------------------------------------
const mockMutate = vi.fn();

vi.mock("~/lib/trpc/client.js", () => ({
  trpc: {
    user: {
      setColumnPreferences: {
        useMutation: () => ({ mutate: mockMutate }),
      },
    },
  },
}));

const { useViewPrefs } = await import("./useViewPrefs.js");

// ---------------------------------------------------------------------------
// localStorage stub — isolate tests from real browser storage
// ---------------------------------------------------------------------------
function createLocalStorageStub() {
  let store: Record<string, string> = {};
  return {
    getItem: vi.fn((key: string) => store[key] ?? null),
    setItem: vi.fn((key: string, value: string) => {
      store[key] = value;
    }),
    removeItem: vi.fn((key: string) => {
      delete store[key];
    }),
    clear: vi.fn(() => {
      store = {};
    }),
  };
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("useViewPrefs", () => {
  let lsStub: ReturnType<typeof createLocalStorageStub>;

  beforeEach(() => {
    vi.useFakeTimers();
    lsStub = createLocalStorageStub();
    vi.spyOn(window, "localStorage", "get").mockReturnValue(lsStub as unknown as Storage);
    mockMutate.mockReset();
  });

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

  // -------------------------------------------------------------------------
  // Initial state
  // -------------------------------------------------------------------------
  describe("initial state", () => {
    it("savedSort is null when localStorage is empty", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      expect(result.current.savedSort).toBeNull();
    });

    it("rowOrder is an empty array when localStorage is empty", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      expect(result.current.rowOrder).toEqual([]);
    });

    it("reads savedSort from localStorage on mount", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ sort: { field: "name", dir: "asc" } }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      expect(result.current.savedSort).toEqual({ field: "name", dir: "asc" });
    });

    it("reads rowOrder from localStorage on mount", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ rowOrder: ["b", "a", "c"] }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      expect(result.current.rowOrder).toEqual(["b", "a", "c"]);
    });

    it("reads from the correct localStorage key for the view", () => {
      renderHook(() => useViewPrefs("projects"));
      expect(lsStub.getItem).toHaveBeenCalledWith("viewprefs_projects");
    });

    it("handles malformed JSON in localStorage gracefully", () => {
      lsStub.getItem.mockReturnValue("{broken json{{");
      const { result } = renderHook(() => useViewPrefs("resources"));
      expect(result.current.savedSort).toBeNull();
      expect(result.current.rowOrder).toEqual([]);
    });
  });

  // -------------------------------------------------------------------------
  // setSavedSort
  // -------------------------------------------------------------------------
  describe("setSavedSort", () => {
    it("updates savedSort in state immediately", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "age", dir: "desc" });
      });
      expect(result.current.savedSort).toEqual({ field: "age", dir: "desc" });
    });

    it("persists the sort to localStorage", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "age", dir: "asc" });
      });
      const written = JSON.parse(lsStub.setItem.mock.calls[0]![1] as string) as unknown;
      expect(written).toMatchObject({ sort: { field: "age", dir: "asc" } });
    });

    it("removes the sort key from localStorage when set to null", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ sort: { field: "name", dir: "asc" } }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort(null);
      });
      const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!;
      const written = JSON.parse(lastCall[1] as string) as Record<string, unknown>;
      expect(written).not.toHaveProperty("sort");
    });

    it("does not call the server mutation before the debounce delay", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "name", dir: "asc" });
      });
      expect(mockMutate).not.toHaveBeenCalled();
    });

    it("calls the server mutation after the 600 ms debounce fires", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "name", dir: "asc" });
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ view: "resources", sort: { field: "name", dir: "asc" } }),
      );
    });

    it("debounces multiple rapid calls — only fires once after the last call", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "name", dir: "asc" });
        result.current.setSavedSort({ field: "age", dir: "desc" });
        result.current.setSavedSort({ field: "status", dir: "asc" });
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledOnce();
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ sort: { field: "status", dir: "asc" } }),
      );
    });

    it("sends null sort to the server mutation when cleared", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort(null);
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ view: "resources", sort: null }),
      );
    });
  });

  // -------------------------------------------------------------------------
  // setRowOrder
  // -------------------------------------------------------------------------
  describe("setRowOrder", () => {
    it("updates rowOrder in state immediately", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["c", "a", "b"]);
      });
      expect(result.current.rowOrder).toEqual(["c", "a", "b"]);
    });

    it("persists the rowOrder to localStorage", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["c", "a", "b"]);
      });
      const written = JSON.parse(
        lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]![1] as string,
      ) as Record<string, unknown>;
      expect(written).toMatchObject({ rowOrder: ["c", "a", "b"] });
    });

    it("removes the rowOrder key from localStorage when set to an empty array", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ rowOrder: ["a", "b"] }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder([]);
      });
      const lastCall = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!;
      const written = JSON.parse(lastCall[1] as string) as Record<string, unknown>;
      expect(written).not.toHaveProperty("rowOrder");
    });

    it("does not call the server mutation before the debounce delay", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["a", "b"]);
      });
      expect(mockMutate).not.toHaveBeenCalled();
    });

    it("calls the server mutation after the 600 ms debounce fires", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["a", "b"]);
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ view: "resources", rowOrder: ["a", "b"] }),
      );
    });

    it("sends null rowOrder to the server mutation when the array is empty", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder([]);
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ view: "resources", rowOrder: null }),
      );
    });

    it("debounces multiple rapid drag reorders into a single mutation", () => {
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["a", "b", "c"]);
        result.current.setRowOrder(["b", "a", "c"]);
        result.current.setRowOrder(["c", "b", "a"]);
      });
      act(() => {
        vi.advanceTimersByTime(600);
      });
      expect(mockMutate).toHaveBeenCalledOnce();
      expect(mockMutate).toHaveBeenCalledWith(
        expect.objectContaining({ rowOrder: ["c", "b", "a"] }),
      );
    });
  });

  // -------------------------------------------------------------------------
  // localStorage key isolation per view
  // -------------------------------------------------------------------------
  describe("localStorage isolation between views", () => {
    it("writes to the correct key for 'projects' view", () => {
      const { result } = renderHook(() => useViewPrefs("projects"));
      act(() => {
        result.current.setSavedSort({ field: "name", dir: "asc" });
      });
      expect(lsStub.setItem).toHaveBeenCalledWith("viewprefs_projects", expect.any(String));
    });

    it("writes to the correct key for 'users' view", () => {
      const { result } = renderHook(() => useViewPrefs("users"));
      act(() => {
        result.current.setRowOrder(["1", "2"]);
      });
      expect(lsStub.setItem).toHaveBeenCalledWith("viewprefs_users", expect.any(String));
    });
  });

  // -------------------------------------------------------------------------
  // Interaction: sort and rowOrder coexist in localStorage
  // -------------------------------------------------------------------------
  describe("combined sort + rowOrder state", () => {
    it("preserves existing sort when rowOrder is updated", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ sort: { field: "name", dir: "asc" } }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setRowOrder(["b", "a"]);
      });
      const lastCall1 = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!;
      const written = JSON.parse(lastCall1[1] as string) as Record<string, unknown>;
      expect(written).toMatchObject({
        sort: { field: "name", dir: "asc" },
        rowOrder: ["b", "a"],
      });
    });

    it("preserves existing rowOrder when sort is updated", () => {
      lsStub.getItem.mockReturnValue(JSON.stringify({ rowOrder: ["x", "y"] }));
      const { result } = renderHook(() => useViewPrefs("resources"));
      act(() => {
        result.current.setSavedSort({ field: "date", dir: "desc" });
      });
      const lastCall2 = lsStub.setItem.mock.calls[lsStub.setItem.mock.calls.length - 1]!;
      const written2 = JSON.parse(lastCall2[1] as string) as Record<string, unknown>;
      expect(written2).toMatchObject({
        sort: { field: "date", dir: "desc" },
        rowOrder: ["x", "y"],
      });
    });
  });
});
