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

// ---------------------------------------------------------------------------
// Mock next/navigation — registered before module import so the factory
// replaces the real module at resolve time.
// ---------------------------------------------------------------------------
const mockReplace = vi.fn();
let mockSearchParamsEntries: [string, string][] = [];
let mockPathname = "/projects";

vi.mock("next/navigation", () => {
  return {
    useRouter: () => ({ replace: mockReplace }),
    usePathname: () => mockPathname,
    useSearchParams: () => {
      const raw = mockSearchParamsEntries
        .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
        .join("&");
      return new URLSearchParams(raw);
    },
  };
});

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

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function setSearchParams(entries: [string, string][]) {
  mockSearchParamsEntries = entries;
}

function lastReplaceUrl(): string {
  const calls = mockReplace.mock.calls;
  return calls[calls.length - 1]![0] as string;
}

function lastReplaceOpts(): unknown {
  const calls = mockReplace.mock.calls;
  return calls[calls.length - 1]![1];
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("useUrlFilters", () => {
  beforeEach(() => {
    mockReplace.mockReset();
    mockSearchParamsEntries = [];
    mockPathname = "/projects";
  });

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

  // -------------------------------------------------------------------------
  // Initial state — all defaults
  // -------------------------------------------------------------------------
  describe("initial state — no URL params", () => {
    it("returns default values when no params are in the URL", () => {
      const defaults = { search: "", status: "ALL" };
      const { result } = renderHook(() => useUrlFilters(defaults));
      const [filters] = result.current;
      expect(filters.search).toBe("");
      expect(filters.status).toBe("ALL");
    });

    it("returns a tuple with filters as first element and setFilters as second", () => {
      const { result } = renderHook(() => useUrlFilters({ q: "" }));
      expect(Array.isArray(result.current)).toBe(true);
      expect(result.current).toHaveLength(2);
      expect(typeof result.current[1]).toBe("function");
    });

    it("returns default for every key when URL is empty", () => {
      const defaults = { a: "x", b: "y", c: "z" };
      const { result } = renderHook(() => useUrlFilters(defaults));
      const [filters] = result.current;
      expect(filters).toEqual({ a: "x", b: "y", c: "z" });
    });
  });

  // -------------------------------------------------------------------------
  // Reading values from URL params
  // -------------------------------------------------------------------------
  describe("reading values from URL params", () => {
    it("reads a single param from the URL", () => {
      setSearchParams([["search", "dragon"]]);
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      const [filters] = result.current;
      expect(filters.search).toBe("dragon");
    });

    it("reads multiple params from the URL", () => {
      setSearchParams([
        ["search", "alice"],
        ["status", "ACTIVE"],
      ]);
      const { result } = renderHook(() => useUrlFilters({ search: "", status: "" }));
      const [filters] = result.current;
      expect(filters.search).toBe("alice");
      expect(filters.status).toBe("ACTIVE");
    });

    it("falls back to default when only some params are set", () => {
      setSearchParams([["status", "DONE"]]);
      const { result } = renderHook(() => useUrlFilters({ search: "fallback", status: "" }));
      const [filters] = result.current;
      expect(filters.search).toBe("fallback");
      expect(filters.status).toBe("DONE");
    });

    it("ignores URL params that are not in the defaults object", () => {
      setSearchParams([
        ["search", "foo"],
        ["extra", "ignored"],
      ]);
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      const [filters] = result.current;
      expect(Object.keys(filters)).not.toContain("extra");
    });

    it("uses URL value even when it matches the default value", () => {
      setSearchParams([["status", "ALL"]]);
      const { result } = renderHook(() => useUrlFilters({ status: "ALL" }));
      const [filters] = result.current;
      // The URL value takes precedence over defaults (same result either way)
      expect(filters.status).toBe("ALL");
    });
  });

  // -------------------------------------------------------------------------
  // setFilters — setting a new value
  // -------------------------------------------------------------------------
  describe("setFilters — setting values", () => {
    it("calls router.replace after setFilters is called", () => {
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: "robot" });
      });
      expect(mockReplace).toHaveBeenCalledOnce();
    });

    it("includes the updated param in the URL", () => {
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: "robot" });
      });
      expect(lastReplaceUrl()).toContain("search=robot");
    });

    it("calls router.replace with scroll: false", () => {
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: "x" });
      });
      expect(lastReplaceOpts()).toEqual({ scroll: false });
    });

    it("uses the current pathname in the replaced URL", () => {
      mockPathname = "/timeline";
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: "test" });
      });
      expect(lastReplaceUrl()).toMatch(/^\/timeline\?/);
    });

    it("preserves existing unrelated params when setting a new value", () => {
      setSearchParams([["status", "ACTIVE"]]);
      const { result } = renderHook(() => useUrlFilters({ search: "", status: "ALL" }));
      act(() => {
        result.current[1]({ search: "hello" });
      });
      const url = lastReplaceUrl();
      expect(url).toContain("search=hello");
      expect(url).toContain("status=ACTIVE");
    });

    it("can update multiple keys in one call", () => {
      const { result } = renderHook(() => useUrlFilters({ search: "", status: "" }));
      act(() => {
        result.current[1]({ search: "foo", status: "DONE" });
      });
      const url = lastReplaceUrl();
      expect(url).toContain("search=foo");
      expect(url).toContain("status=DONE");
    });
  });

  // -------------------------------------------------------------------------
  // setFilters — removing default values keeps URL clean
  // -------------------------------------------------------------------------
  describe("setFilters — default values are not written to URL", () => {
    it("deletes the param when the new value matches the default", () => {
      setSearchParams([["status", "ACTIVE"]]);
      const { result } = renderHook(() => useUrlFilters({ status: "ALL" }));
      act(() => {
        result.current[1]({ status: "ALL" }); // setting to default
      });
      const url = lastReplaceUrl();
      expect(url).not.toContain("status=");
    });

    it("deletes the param when the new value is undefined", () => {
      setSearchParams([["search", "hello"]]);
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: undefined as unknown as string });
      });
      const url = lastReplaceUrl();
      expect(url).not.toContain("search=");
    });

    it("keeps URL clean when all values revert to defaults", () => {
      setSearchParams([["status", "DONE"]]);
      const { result } = renderHook(() => useUrlFilters({ status: "ALL" }));
      act(() => {
        result.current[1]({ status: "ALL" });
      });
      // After removing the only param the query string should be empty
      expect(lastReplaceUrl()).toMatch(/^\/projects(\?)?$/);
    });
  });

  // -------------------------------------------------------------------------
  // setFilters — non-default values are persisted
  // -------------------------------------------------------------------------
  describe("setFilters — non-default values are written to URL", () => {
    it("writes a non-default string value to the URL", () => {
      const { result } = renderHook(() => useUrlFilters({ status: "ALL" }));
      act(() => {
        result.current[1]({ status: "ACTIVE" });
      });
      expect(lastReplaceUrl()).toContain("status=ACTIVE");
    });

    it("replaces an existing URL param with a new non-default value", () => {
      setSearchParams([["status", "ACTIVE"]]);
      const { result } = renderHook(() => useUrlFilters({ status: "ALL" }));
      act(() => {
        result.current[1]({ status: "DONE" });
      });
      const url = lastReplaceUrl();
      expect(url).toContain("status=DONE");
      expect(url).not.toContain("status=ACTIVE");
    });
  });

  // -------------------------------------------------------------------------
  // setFilters stability — the updater function is stable across renders
  // -------------------------------------------------------------------------
  describe("setFilters reference stability", () => {
    it("returns a function on every render (setFilters is always callable)", () => {
      const { result, rerender } = renderHook(() => useUrlFilters({ search: "" }));
      expect(typeof result.current[1]).toBe("function");
      rerender();
      expect(typeof result.current[1]).toBe("function");
    });

    it("setFilters remains a function after multiple rerenders", () => {
      const { result, rerender } = renderHook(() => useUrlFilters({ search: "" }));
      rerender();
      rerender();
      expect(typeof result.current[1]).toBe("function");
    });
  });

  // -------------------------------------------------------------------------
  // Edge cases
  // -------------------------------------------------------------------------
  describe("edge cases", () => {
    it("handles an empty defaults object gracefully", () => {
      const { result } = renderHook(() => useUrlFilters({}));
      const [filters] = result.current;
      expect(filters).toEqual({});
    });

    it("does not call router.replace on initial render", () => {
      renderHook(() => useUrlFilters({ search: "default" }));
      expect(mockReplace).not.toHaveBeenCalled();
    });

    it("handles a single key with an empty string default", () => {
      const { result } = renderHook(() => useUrlFilters({ q: "" }));
      const [filters] = result.current;
      expect(filters.q).toBe("");
    });

    it("produces a URL that starts with the pathname", () => {
      mockPathname = "/resources";
      const { result } = renderHook(() => useUrlFilters({ search: "" }));
      act(() => {
        result.current[1]({ search: "test" });
      });
      expect(lastReplaceUrl()).toMatch(/^\/resources/);
    });
  });
});
