import { fireEvent, render } from "@testing-library/react";
import { vi } from "vitest";

import { AnchorNavigation } from "../AnchorNavigation";
import AnchorNavigationStatic from "../AnchorNavigation.static";

const basicItems = [
  { label: "Key Features", href: "#features", isActive: true },
  { label: "Pricing", href: "#pricing" },
  { label: "Getting Started", href: "#getting-started" },
  { label: "Contact", href: "#contact" },
];

const moreItems = [
  { label: "Overview", href: "#overview", isActive: true },
  { label: "Features", href: "#features" },
  { label: "Documentation", href: "#docs" },
  { label: "API Reference", href: "#api" },
  { label: "Examples", href: "#examples" },
  { label: "Support", href: "#support" },
];

const initializeAnchorNavigation = (container) => {
  const anchorNavigationElement = container.querySelector(
    "[data-anchor-navigation]",
  );

  if (
    anchorNavigationElement &&
    !AnchorNavigationStatic.getInstance(anchorNavigationElement)
  ) {
    new AnchorNavigationStatic(anchorNavigationElement);
  }

  return anchorNavigationElement;
};

describe("rendering AnchorNavigation", () => {
  const mockFinePointer = () =>
    vi.spyOn(window, "matchMedia").mockImplementation((query) => ({
      matches: query === "(pointer: fine)",
      media: query,
      onchange: null,
      addListener: vi.fn(),
      removeListener: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    }));

  describe("initial state", () => {
    it("has default class anchor-navigation", () => {
      const { getByTestId } = render(
        <AnchorNavigation data-testid="test-id" items={basicItems} />,
      );
      expect(getByTestId("test-id")).toHaveClass("anchor-navigation");
    });

    it("has data-anchor-navigation attribute", () => {
      const { getByTestId } = render(
        <AnchorNavigation data-testid="test-id" items={basicItems} />,
      );
      expect(getByTestId("test-id")).toHaveAttribute("data-anchor-navigation");
    });

    it("renders container with anchor-navigation__content class", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);
      expect(
        container.querySelector(".anchor-navigation__content"),
      ).toBeInTheDocument();
    });

    it("renders grid with correct classes", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);
      const grid = container.querySelector(".anchor-navigation__content-left");
      expect(grid).toBeInTheDocument();
      expect(grid).toHaveClass("list-inline");
      expect(grid).toHaveClass("horizontal-scroll");
      expect(grid).toHaveClass("mb-none");
    });
  });

  describe("passed props", () => {
    it("renders all navigation items", () => {
      const { getByText } = render(<AnchorNavigation items={basicItems} />);

      expect(getByText("Key Features")).toBeInTheDocument();
      expect(getByText("Pricing")).toBeInTheDocument();
      expect(getByText("Getting Started")).toBeInTheDocument();
      expect(getByText("Contact")).toBeInTheDocument();
    });

    it("renders navigation items as links with correct href", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);

      const featuresLink = container.querySelector('a[href="#features"]');
      const pricingLink = container.querySelector('a[href="#pricing"]');

      expect(featuresLink).toBeInTheDocument();
      expect(featuresLink).toHaveTextContent("Key Features");
      expect(pricingLink).toBeInTheDocument();
      expect(pricingLink).toHaveTextContent("Pricing");
    });

    it("applies active class to items with isActive prop", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);

      const activeLink = container.querySelector('a[href="#features"]');
      const inactiveLink = container.querySelector('a[href="#pricing"]');

      expect(activeLink).toHaveClass("is-active");
      expect(inactiveLink).not.toHaveClass("is-active");
    });

    it("applies anchor-navigation__item class to all links", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);

      const links = container.querySelectorAll(".anchor-navigation__item");
      expect(links).toHaveLength(basicItems.length);
    });

    it("renders multiple items correctly", () => {
      const { getByText } = render(<AnchorNavigation items={moreItems} />);

      expect(getByText("Overview")).toBeInTheDocument();
      expect(getByText("Features")).toBeInTheDocument();
      expect(getByText("Documentation")).toBeInTheDocument();
      expect(getByText("API Reference")).toBeInTheDocument();
      expect(getByText("Examples")).toBeInTheDocument();
      expect(getByText("Support")).toBeInTheDocument();
    });

    it("applies custom className", () => {
      const { getByTestId } = render(
        <AnchorNavigation
          data-testid="test-id"
          items={basicItems}
          className="custom-class"
        />,
      );
      expect(getByTestId("test-id")).toHaveClass("anchor-navigation");
      expect(getByTestId("test-id")).toHaveClass("custom-class");
    });
  });

  describe("children content", () => {
    it("renders children in content-right section when provided", () => {
      const { container, getByText } = render(
        <AnchorNavigation items={basicItems}>
          <div>Additional Content</div>
        </AnchorNavigation>,
      );

      const contentRight = container.querySelector(
        ".anchor-navigation__content-right",
      );
      expect(contentRight).toBeInTheDocument();
      expect(getByText("Additional Content")).toBeInTheDocument();
    });

    it("does not render content-right section when no children", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);

      const contentRight = container.querySelector(
        ".anchor-navigation__content-right",
      );
      expect(contentRight).not.toBeInTheDocument();
    });

    it("renders complex children content", () => {
      const { getByText, getByRole } = render(
        <AnchorNavigation items={basicItems}>
          <div className="pricing">
            <div className="bold">2 €</div>
            <div>No commitment</div>
          </div>
          <button>Get Started</button>
        </AnchorNavigation>,
      );

      expect(getByText("2 €")).toBeInTheDocument();
      expect(getByText("No commitment")).toBeInTheDocument();
      expect(getByRole("button", { name: "Get Started" })).toBeInTheDocument();
    });
  });

  describe("displayName", () => {
    it("has correct displayName", () => {
      expect(AnchorNavigation.displayName).toBe("AnchorNavigation");
    });
  });

  describe("static behavior", () => {
    it("adds draggable class when left content overflows on desktop", () => {
      const matchMediaSpy = mockFinePointer();
      const { container } = render(<AnchorNavigation items={moreItems} />);
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );

      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 200,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 480,
      });

      anchorNavigation?.update();

      expect(contentLeft).toHaveClass("is-draggable");

      anchorNavigation?.destroy();
      matchMediaSpy.mockRestore();
    });

    it("prevents anchor activation after drag scrolling", () => {
      const matchMediaSpy = mockFinePointer();
      const section = document.createElement("section");
      section.id = "features";
      document.body.appendChild(section);

      const scrollToSpy = vi
        .spyOn(window, "scrollTo")
        .mockImplementation(() => {});
      const { container } = render(<AnchorNavigation items={basicItems} />);
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );
      const featureLink = container.querySelector('a[href="#features"]');
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );

      let scrollLeft = 0;
      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 200,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 480,
      });
      Object.defineProperty(contentLeft, "scrollLeft", {
        configurable: true,
        get: () => scrollLeft,
        set: (value) => {
          scrollLeft = value;
        },
      });
      contentLeft.scrollTo = vi.fn(({ left }) => {
        scrollLeft = left;
      });

      anchorNavigation?.update();

      fireEvent.mouseDown(contentLeft, { button: 0, clientX: 180 });
      fireEvent.mouseMove(window, { clientX: 120 });
      fireEvent.mouseUp(window);
      fireEvent.click(featureLink);

      expect(scrollLeft).toBe(60);
      expect(scrollToSpy).not.toHaveBeenCalled();

      anchorNavigation?.destroy();
      scrollToSpy.mockRestore();
      matchMediaSpy.mockRestore();
      section.remove();
    });

    it("prevents mousedown default on non-interactive content-right area", () => {
      const { container } = render(
        <AnchorNavigation items={basicItems}>
          <div className="align-lg-right mb-none">
            <span>16 €</span>
            <span className="text-secondary">S viazanostou 24 mesiacov</span>
          </div>
          <a href="/senior-pausal/chcem-senior">Kupit Senior pausal</a>
        </AnchorNavigation>,
      );
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const infoBlock = container.querySelector(".align-lg-right");

      const downEvent = new MouseEvent("mousedown", {
        bubbles: true,
        cancelable: true,
        button: 0,
      });
      infoBlock?.dispatchEvent(downEvent);

      expect(downEvent.defaultPrevented).toBe(true);

      anchorNavigation?.destroy();
    });

    it("does not prevent mousedown default on interactive content-right link", () => {
      const { container } = render(
        <AnchorNavigation items={basicItems}>
          <a href="/senior-pausal/chcem-senior">Kupit Senior pausal</a>
        </AnchorNavigation>,
      );
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const ctaLink = container.querySelector(
        'a[href="/senior-pausal/chcem-senior"]',
      );

      const downEvent = new MouseEvent("mousedown", {
        bubbles: true,
        cancelable: true,
        button: 0,
      });
      ctaLink?.dispatchEvent(downEvent);

      expect(downEvent.defaultPrevented).toBe(false);

      anchorNavigation?.destroy();
    });

    it("scrolls active item using the left content viewport width", () => {
      const section = document.createElement("section");
      section.id = "pricing";
      document.body.appendChild(section);

      const { container } = render(
        <AnchorNavigation items={basicItems}>
          <div>5,00 €</div>
          <button>CTA</button>
        </AnchorNavigation>,
      );
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );
      const activeLink = container.querySelector('a[href="#pricing"]');
      const scrollToSpy = vi.fn();

      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 180,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 600,
      });
      Object.defineProperty(contentLeft, "scrollLeft", {
        configurable: true,
        value: 0,
        writable: true,
      });
      Object.defineProperty(contentLeft, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 0,
          right: 180,
          width: 180,
          height: 0,
          x: 0,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      Object.defineProperty(activeLink, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 220,
          right: 300,
          width: 80,
          height: 0,
          x: 220,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      contentLeft.scrollTo = scrollToSpy;

      anchorNavigation.initScrollSpy("pricing");

      expect(scrollToSpy).toHaveBeenCalledWith({
        left: 170,
        behavior: "smooth",
      });

      anchorNavigation?.destroy();
      section.remove();
    });

    it("centers clicked active item even when it is already visible", () => {
      const section = document.createElement("section");
      section.id = "pricing";
      document.body.appendChild(section);

      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );
      const activeLink = container.querySelector('a[href="#pricing"]');
      const scrollToSpy = vi.fn();

      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 200,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 600,
      });
      Object.defineProperty(contentLeft, "scrollLeft", {
        configurable: true,
        value: 180,
        writable: true,
      });
      Object.defineProperty(contentLeft, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 0,
          right: 200,
          width: 200,
          height: 0,
          x: 0,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      Object.defineProperty(activeLink, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 40,
          right: 120,
          width: 80,
          height: 0,
          x: 40,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      contentLeft.scrollTo = scrollToSpy;

      anchorNavigation.initScrollSpy("pricing");

      expect(scrollToSpy).toHaveBeenCalledWith({
        left: 160,
        behavior: "smooth",
      });

      anchorNavigation?.destroy();
      section.remove();
    });

    it("centers scroll-spy active item even when it is already visible", () => {
      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );
      const activeLink = container.querySelector('a[href="#pricing"]');
      const scrollToSpy = vi.fn();

      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 200,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 600,
      });
      Object.defineProperty(contentLeft, "scrollLeft", {
        configurable: true,
        value: 180,
        writable: true,
      });
      Object.defineProperty(contentLeft, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 0,
          right: 200,
          width: 200,
          height: 0,
          x: 0,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      Object.defineProperty(activeLink, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 0,
          bottom: 0,
          left: 40,
          right: 120,
          width: 80,
          height: 0,
          x: 40,
          y: 0,
          toJSON: () => ({}),
        }),
      });
      contentLeft.scrollTo = scrollToSpy;

      anchorNavigation.scrollActiveLinkIntoView(contentLeft, activeLink, false);

      expect(scrollToSpy).toHaveBeenCalledWith({
        left: 160,
        behavior: "smooth",
      });

      anchorNavigation?.destroy();
    });

    it("does not duplicate anchor click handling after update", () => {
      const section = document.createElement("section");
      section.id = "features";
      document.body.appendChild(section);

      const scrollToSpy = vi
        .spyOn(window, "scrollTo")
        .mockImplementation(() => {});
      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const featureLink = container.querySelector('a[href="#features"]');
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );

      contentLeft.scrollTo = vi.fn();

      fireEvent.click(featureLink);
      anchorNavigation?.update();
      fireEvent.click(featureLink);

      expect(scrollToSpy).toHaveBeenCalledTimes(2);

      anchorNavigation?.destroy();
      scrollToSpy.mockRestore();
      section.remove();
    });

    it("preserves current path and query when updating URL hash", () => {
      const section = document.createElement("section");
      section.id = "features";
      document.body.appendChild(section);

      window.history.replaceState(null, "", "/senior-pausal?foo=bar");

      const pushStateSpy = vi
        .spyOn(window.history, "pushState")
        .mockImplementation(() => {});
      const scrollToSpy = vi
        .spyOn(window, "scrollTo")
        .mockImplementation(() => {});
      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const featureLink = container.querySelector('a[href="#features"]');

      fireEvent.click(featureLink);

      expect(pushStateSpy).toHaveBeenCalledWith(
        null,
        "",
        "/senior-pausal?foo=bar#features",
      );

      anchorNavigation?.destroy();
      pushStateSpy.mockRestore();
      scrollToSpy.mockRestore();
      section.remove();
    });

    it("prefers document scroll-padding-top over internal sticky offset", () => {
      const section = document.createElement("section");
      section.id = "pricing";
      Object.defineProperty(section, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 600 - window.scrollY,
          bottom: 700 - window.scrollY,
          left: 0,
          right: 0,
          width: 0,
          height: 100,
          x: 0,
          y: 600,
          toJSON: () => ({}),
        }),
      });
      document.body.appendChild(section);

      const megamenu = document.createElement("div");
      megamenu.setAttribute("data-megamenu", "");
      Object.defineProperty(megamenu, "offsetHeight", {
        configurable: true,
        value: 120,
      });
      document.body.appendChild(megamenu);

      Object.defineProperty(window, "scrollY", {
        configurable: true,
        value: 0,
        writable: true,
      });

      const previousScrollPaddingTop =
        document.documentElement.style.scrollPaddingTop;
      document.documentElement.style.scrollPaddingTop = "210px";

      const scrollToSpy = vi
        .spyOn(window, "scrollTo")
        .mockImplementation((options) => {
          if (typeof options === "object" && typeof options.top === "number") {
            window.scrollY = options.top;
          }
        });

      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const pricingLink = container.querySelector('a[href="#pricing"]');

      Object.defineProperty(anchorNavigationElement, "offsetHeight", {
        configurable: true,
        value: 50,
      });

      fireEvent.click(pricingLink);

      expect(scrollToSpy).toHaveBeenNthCalledWith(1, {
        top: 390,
        behavior: "smooth",
      });

      anchorNavigation?.destroy();
      scrollToSpy.mockRestore();
      document.documentElement.style.scrollPaddingTop =
        previousScrollPaddingTop;
      megamenu.remove();
      section.remove();
    });

    it("realigns anchor after sticky offset changes during smooth scroll", () => {
      vi.useFakeTimers();

      const section = document.createElement("section");
      section.id = "pricing";
      Object.defineProperty(section, "getBoundingClientRect", {
        configurable: true,
        value: () => ({
          top: 600 - window.scrollY,
          bottom: 700 - window.scrollY,
          left: 0,
          right: 0,
          width: 0,
          height: 100,
          x: 0,
          y: 600,
          toJSON: () => ({}),
        }),
      });
      document.body.appendChild(section);

      const megamenu = document.createElement("div");
      megamenu.setAttribute("data-megamenu", "");
      let megamenuHeight = 120;
      Object.defineProperty(megamenu, "offsetHeight", {
        configurable: true,
        get: () => megamenuHeight,
      });
      document.body.appendChild(megamenu);

      Object.defineProperty(window, "scrollY", {
        configurable: true,
        value: 0,
        writable: true,
      });

      const scrollToSpy = vi
        .spyOn(window, "scrollTo")
        .mockImplementation((options) => {
          if (typeof options === "object" && typeof options.top === "number") {
            window.scrollY = options.top;
          }
        });

      const { container } = render(<AnchorNavigation items={basicItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const pricingLink = container.querySelector('a[href="#pricing"]');

      Object.defineProperty(anchorNavigationElement, "offsetHeight", {
        configurable: true,
        value: 50,
      });

      fireEvent.click(pricingLink);

      expect(scrollToSpy).toHaveBeenNthCalledWith(1, {
        top: 430,
        behavior: "smooth",
      });

      megamenuHeight = 80;
      fireEvent.scroll(window);
      vi.advanceTimersByTime(
        AnchorNavigationStatic.SCROLL_END_DEBOUNCE_MS + 20,
      );

      expect(scrollToSpy).toHaveBeenNthCalledWith(2, {
        top: 470,
        behavior: "smooth",
      });

      anchorNavigation?.destroy();
      scrollToSpy.mockRestore();
      megamenu.remove();
      section.remove();
      vi.useRealTimers();
    });

    it("toggles left and right overflow classes based on scroll position", () => {
      const { container } = render(<AnchorNavigation items={moreItems} />);
      const anchorNavigationElement = initializeAnchorNavigation(container);
      const anchorNavigation = AnchorNavigationStatic.getInstance(
        anchorNavigationElement,
      );
      const content = container.querySelector(".anchor-navigation__content");
      const contentLeft = container.querySelector(
        ".anchor-navigation__content-left",
      );

      let scrollLeft = 0;
      Object.defineProperty(contentLeft, "clientWidth", {
        configurable: true,
        value: 200,
      });
      Object.defineProperty(contentLeft, "scrollWidth", {
        configurable: true,
        value: 500,
      });
      Object.defineProperty(contentLeft, "scrollLeft", {
        configurable: true,
        get: () => scrollLeft,
        set: (value) => {
          scrollLeft = value;
        },
      });

      anchorNavigation?.update();

      expect(content).not.toHaveClass("has-left-overflow");
      expect(content).toHaveClass("has-right-overflow");

      scrollLeft = 300;
      fireEvent.scroll(contentLeft);

      expect(content).toHaveClass("has-left-overflow");
      expect(content).not.toHaveClass("has-right-overflow");

      anchorNavigation?.destroy();
    });
  });
});
