import { createRef } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { AccordionLinkElementType } from "./AccordionLink";
import { Accordion } from ".";
import { mockMatchMedia } from "../../utils/mockMatchMedia";

const CONTAINER_CLASS_NAME = "mobius-accordion";
const CONTENT_CONTAINER_CLASS_NAME = "mobius-accordion__content-container";
const HEADER_CLASS_NAME = "mobius-accordion__header";
const LINK_CLASS_NAME = "mobius-accordion__link";
const OPEN_CLASS_NAME = "--is-open";
const OPEN_TEXT = "See more";
const CLOSE_TEXT = "See less";

describe("Accordion", () => {
  beforeEach(() => {
    mockMatchMedia(true);
  });

  it("should render without errors", () => {
    const view = render(<Accordion>test</Accordion>);

    expect(view).toBeTruthy();
  });

  it("should open when clicked", () => {
    const { container } = render(<Accordion>test content</Accordion>);
    const toggle = screen.getByText(OPEN_TEXT);
    const accordionContainer = container.firstChild;

    expect(accordionContainer).not.toHaveClass(OPEN_CLASS_NAME);
    expect(toggle).toBeInTheDocument();

    fireEvent.click(toggle);

    expect(accordionContainer).toHaveClass(OPEN_CLASS_NAME);
    expect(screen.getByText(CLOSE_TEXT)).toBeInTheDocument();

    fireEvent.click(toggle);

    expect(accordionContainer).not.toHaveClass(OPEN_CLASS_NAME);
    expect(toggle).toBeInTheDocument();
  });

  it("should render chevron after link", () => {
    render(<Accordion>test</Accordion>);

    const toggle = screen.getByRole("button");

    expect(toggle.firstChild).toHaveTextContent(OPEN_TEXT);
    expect(toggle.lastChild).toHaveAttribute("data-icon", "chevron-down");
  });

  describe("when headerPosition is set to top", () => {
    it("renders the toggle above all other content", () => {
      const { container } = render(
        <Accordion headerPosition="top">test content</Accordion>,
      );
      const accordion = container.firstChild;

      expect(accordion?.firstChild).toHaveClass(LINK_CLASS_NAME);
      expect(accordion?.lastChild).toHaveClass(CONTENT_CONTAINER_CLASS_NAME);
    });
  });

  describe("when headerPosition is set to bottom", () => {
    it("renders the toggle underneath all other content", () => {
      const { container } = render(
        <Accordion headerPosition="bottom">test content</Accordion>,
      );
      const accordion = container.firstChild;

      expect(accordion?.firstChild).toHaveClass(CONTENT_CONTAINER_CLASS_NAME);
      expect(accordion?.lastChild).toHaveClass(LINK_CLASS_NAME);
    });
  });

  it("respects startOpen prop and is expanded", () => {
    render(<Accordion startOpen>test content</Accordion>);

    expect(screen.getByText(CLOSE_TEXT)).toBeInTheDocument();
  });

  describe("uses correct class names", () => {
    it("uses mobius class", () => {
      const { container } = render(<Accordion>test content</Accordion>);

      expect(container.firstChild).toHaveClass("mobius");
    });

    it("uses base Mobius class", () => {
      const { container } = render(<Accordion>Sample text</Accordion>);

      expect(container.firstChild).toHaveClass(CONTAINER_CLASS_NAME);
    });

    it("includes custom class name", () => {
      const customClassName = "my-class";

      const { container } = render(
        <Accordion className={customClassName}>Sample Text</Accordion>,
      );

      expect(container.firstChild).toHaveClass(customClassName);
      expect(container.firstChild).toHaveClass(CONTAINER_CLASS_NAME);
    });
  });

  describe("given headerChildren prop has content", () => {
    it("renders the content", () => {
      const buttonText = "Go";

      const { container } = render(
        <Accordion headerChildren={<button type="button">{buttonText}</button>}>
          Sample Text
        </Accordion>,
      );

      const headerContainer =
        container.getElementsByClassName(HEADER_CLASS_NAME);

      expect(screen.getByText(buttonText)).toBeInTheDocument();
      expect(headerContainer).toHaveLength(1);
    });
  });

  describe("given headerChildren is not defined", () => {
    it("it does not render the header component", () => {
      const { container } = render(<Accordion>Sample Text</Accordion>);

      const headerContainer =
        container.getElementsByClassName(HEADER_CLASS_NAME);

      expect(headerContainer).toHaveLength(0);
    });
  });

  it("renders with aria-expanded attribute", () => {
    render(<Accordion>Sample Text</Accordion>);

    const button = screen.getByRole("button");

    expect(button).toHaveAttribute("aria-expanded", "false");

    fireEvent.click(button);

    expect(button).toHaveAttribute("aria-expanded", "true");
  });

  describe("providing that startOpen prop changes over time", () => {
    it("renders the correct state", () => {
      const { rerender } = render(<Accordion startOpen>Sample Text</Accordion>);

      const seeLess = screen.getByText(CLOSE_TEXT);

      expect(seeLess).toBeInTheDocument();

      rerender(<Accordion startOpen={false}>Sample Text</Accordion>);

      const seeMore = screen.getByText(OPEN_TEXT);

      expect(seeMore).toBeInTheDocument();
    });
  });

  describe("events", () => {
    it("calls onChange when accordion is toggled", () => {
      const onChange = vi.fn();

      render(<Accordion onChange={onChange}>Sample Text</Accordion>);

      const button = screen.getByRole("button");

      fireEvent.click(button);

      expect(onChange).toHaveBeenCalledWith(true);

      fireEvent.click(button);

      expect(onChange).toHaveBeenCalledWith(false);
    });

    it("calls onOpen when accordion is opened", () => {
      const onOpen = vi.fn();
      const onClose = vi.fn();

      render(
        <Accordion onOpen={onOpen} onClose={onClose}>
          Sample Text
        </Accordion>,
      );

      const button = screen.getByRole("button");

      fireEvent.click(button);

      expect(onOpen).toHaveBeenCalled();
      expect(onClose).not.toHaveBeenCalled();
    });

    it("calls onClose when accordion is closed", () => {
      const onOpen = vi.fn();
      const onClose = vi.fn();

      render(
        <Accordion startOpen onOpen={onOpen} onClose={onClose}>
          Sample Text
        </Accordion>,
      );

      const button = screen.getByRole("button");

      fireEvent.click(button);

      expect(onClose).toHaveBeenCalled();
      expect(onOpen).not.toHaveBeenCalled();
    });
  });

  describe("given user prefers reduced motion", () => {
    it("runs no animations", () => {
      const { container } = render(<Accordion>Sample Text</Accordion>);

      expect(container.firstChild).not.toHaveClass("--should-animate");
    });
  });

  describe("keyboard navigation", () => {
    it("should open on Enter key", async () => {
      const toggle = vi.fn();
      render(<Accordion onChange={toggle}>Sample text</Accordion>);

      await userEvent.tab();
      await userEvent.keyboard("[Enter]");

      expect(toggle).toHaveBeenCalled();
    });

    it("should open on Space key", async () => {
      const toggle = vi.fn();
      render(<Accordion onChange={toggle}>Sample text</Accordion>);

      await userEvent.tab();
      await userEvent.keyboard("[Space]");

      expect(toggle).toHaveBeenCalled();
    });

    it("should not open on Tab key", async () => {
      const toggle = vi.fn();
      render(<Accordion onChange={toggle}>Sample text</Accordion>);

      await userEvent.tab();
      await userEvent.tab();

      expect(toggle).not.toHaveBeenCalled();
    });
  });

  describe("semantic heading", () => {
    it("uses span by default", () => {
      const toggle = vi.fn();
      const { container } = render(
        <Accordion onChange={toggle}>Sample text</Accordion>,
      );

      const linkContainer = container.getElementsByClassName(LINK_CLASS_NAME);
      const link = linkContainer[0].firstChild;

      expect(link instanceof HTMLSpanElement).toBe(true);
    });

    it.each(["h1", "h2", "h3", "h4", "h5", "h6"])(
      "uses heading type %s",
      headingType => {
        const toggle = vi.fn();
        const { container } = render(
          <Accordion
            onChange={toggle}
            linkElementType={headingType as AccordionLinkElementType}
          >
            Sample text
          </Accordion>,
        );

        const linkContainer = container.getElementsByClassName(LINK_CLASS_NAME);
        const link = linkContainer[0].firstChild;

        expect(link instanceof HTMLHeadingElement).toBe(true);
        expect(link?.nodeName).toBe(headingType.toUpperCase());
      },
    );
  });

  describe("showText and hideText with ReactNode", () => {
    it("renders showText as a ReactNode when closed", () => {
      const showTextNode = (
        <span data-testid="custom-show">
          <strong>Expand</strong> this section
        </span>
      );

      render(<Accordion showText={showTextNode}>Sample text</Accordion>);

      expect(screen.getByTestId("custom-show")).toBeInTheDocument();
      expect(screen.getByText("Expand")).toBeInTheDocument();
      expect(screen.getByText("this section")).toBeInTheDocument();
    });

    it("renders hideText as a ReactNode when open", () => {
      const hideTextNode = (
        <span data-testid="custom-hide">
          <strong>Collapse</strong> this section
        </span>
      );

      render(
        <Accordion hideText={hideTextNode} startOpen>
          Sample text
        </Accordion>,
      );

      expect(screen.getByTestId("custom-hide")).toBeInTheDocument();
      expect(screen.getByText("Collapse")).toBeInTheDocument();
      expect(screen.getByText("this section")).toBeInTheDocument();
    });

    it("toggles between ReactNode showText and hideText", () => {
      const showTextNode = <span data-testid="custom-show">Show more</span>;
      const hideTextNode = <span data-testid="custom-hide">Show less</span>;

      render(
        <Accordion showText={showTextNode} hideText={hideTextNode}>
          Sample text
        </Accordion>,
      );

      // Initially closed - should show showText
      expect(screen.getByTestId("custom-show")).toBeInTheDocument();
      expect(screen.queryByTestId("custom-hide")).not.toBeInTheDocument();

      // Click to open - should show hideText
      const button = screen.getByRole("button");
      fireEvent.click(button);

      expect(screen.getByTestId("custom-hide")).toBeInTheDocument();
      expect(screen.queryByTestId("custom-show")).not.toBeInTheDocument();

      // Click to close - should show showText again
      fireEvent.click(button);

      expect(screen.getByTestId("custom-show")).toBeInTheDocument();
      expect(screen.queryByTestId("custom-hide")).not.toBeInTheDocument();
    });

    it("renders showText with icon as ReactNode", () => {
      const showTextWithIcon = (
        <span data-testid="show-with-icon">
          <span>More details</span>
          <span className="custom-icon">+</span>
        </span>
      );

      render(<Accordion showText={showTextWithIcon}>Sample text</Accordion>);

      expect(screen.getByTestId("show-with-icon")).toBeInTheDocument();
      expect(screen.getByText("More details")).toBeInTheDocument();
      expect(screen.getByText("+")).toBeInTheDocument();
    });

    it("renders hideText with icon as ReactNode", () => {
      const hideTextWithIcon = (
        <span data-testid="hide-with-icon">
          <span>Less details</span>
          <span className="custom-icon">-</span>
        </span>
      );

      render(
        <Accordion hideText={hideTextWithIcon} startOpen>
          Sample text
        </Accordion>,
      );

      expect(screen.getByTestId("hide-with-icon")).toBeInTheDocument();
      expect(screen.getByText("Less details")).toBeInTheDocument();
      expect(screen.getByText("-")).toBeInTheDocument();
    });

    it("works with string values for backward compatibility", () => {
      const customShow = "Expand content";
      const customHide = "Collapse content";

      render(
        <Accordion showText={customShow} hideText={customHide}>
          Sample text
        </Accordion>,
      );

      // Initially closed - should show string showText
      expect(screen.getByText(customShow)).toBeInTheDocument();

      // Click to open - should show string hideText
      const button = screen.getByRole("button");
      fireEvent.click(button);

      expect(screen.getByText(customHide)).toBeInTheDocument();
      expect(screen.queryByText(customShow)).not.toBeInTheDocument();
    });
  });

  it("forwards ref", () => {
    const ref = createRef<HTMLDivElement>();
    render(<Accordion ref={ref}>Text</Accordion>);
    expect(ref.current).toBeInstanceOf(HTMLDivElement);
  });
});
