import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { TextOrHTML } from ".";

const CLASS_NAME = "mobius-text";

describe("TextOrHTML", () => {
  it("should render without errors", () => {
    render(<TextOrHTML text="<p>Sample text</p>" />);
  });

  describe("given text prop", () => {
    it("renders HTML content using dangerouslySetInnerHTML", () => {
      const { container } = render(
        <TextOrHTML text="<strong>Bold text</strong>" />,
      );
      // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
      const span = container.querySelector("span");
      expect(span?.innerHTML).toBe("<strong>Bold text</strong>");
    });

    it("is not applied as element attribute", () => {
      const { container } = render(<TextOrHTML text="<p>Sample</p>" />);
      // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
      const span = container.querySelector("span");
      expect(span).not.toHaveAttribute("text");
    });
  });

  describe("given textWrapper prop", () => {
    it("wraps dangerous HTML in a span inside Text component when textWrapper is true", () => {
      const { container } = render(
        <TextOrHTML
          text="<strong>Bold text</strong>"
          textWrapper
          data-testid="test"
        />,
      );
      const element = screen.getByTestId("test");
      expect(element).toBeInTheDocument();
      // eslint-disable-next-line testing-library/no-node-access, testing-library/no-container
      const span = container.querySelector("span");
      expect(span?.innerHTML).toBe("<strong>Bold text</strong>");
    });

    it("renders dangerous HTML directly on element when textWrapper is false or omitted", () => {
      const { container } = render(
        <TextOrHTML text="<strong>Bold text</strong>" />,
      );
      // eslint-disable-next-line testing-library/no-node-access, testing-library/no-container
      const span = container.querySelector("span");
      expect(span?.innerHTML).toBe("<strong>Bold text</strong>");
    });
  });

  describe("given elementType", () => {
    it("is not applied as element attribute", () => {
      const { container } = render(
        <TextOrHTML htmlElementType="div" text="<p>Sample text</p>" />,
      );
      expect(container.firstChild).not.toHaveAttribute("elementtype", "div");
    });

    it("renders with specified HTML element type", () => {
      const { container } = render(
        <TextOrHTML htmlElementType="div" text="<span>Sample text</span>" />,
      );
      expect(container.firstChild?.nodeName).toBe("DIV");
    });
  });

  describe("uses correct class names", () => {
    it("uses Mobius class", () => {
      render(
        <TextOrHTML textWrapper text="<p>Sample</p>" data-testid="test" />,
      );
      expect(screen.getByTestId("test")).toHaveClass("mobius");
    });

    it("uses base Mobius class", () => {
      render(
        <TextOrHTML textWrapper text="<p>Sample</p>" data-testid="test" />,
      );
      expect(screen.getByTestId("test")).toHaveClass(CLASS_NAME);
    });

    it("includes custom class name", () => {
      render(
        <TextOrHTML
          textWrapper
          className="my-class"
          text="<p>Sample</p>"
          data-testid="test"
        />,
      );
      expect(screen.getByTestId("test")).toHaveClass("my-class");
      expect(screen.getByTestId("test")).toHaveClass(CLASS_NAME);
    });
  });

  describe("variant prop", () => {
    it("applies variant class name", () => {
      render(
        <TextOrHTML
          textWrapper
          variant="h2"
          text="<p>Sample</p>"
          data-testid="test"
        />,
      );
      expect(screen.getByTestId("test")).toHaveClass("--is-h2");
    });
  });

  describe("spacing prop", () => {
    it("applies spacing class name", () => {
      render(
        <TextOrHTML
          textWrapper
          spacing="tight"
          text="<p>Sample</p>"
          data-testid="test"
        />,
      );
      expect(screen.getByTestId("test")).toHaveClass("--has-line-height-tight");
    });
  });

  describe("autoDetect prop", () => {
    it("detects inline HTML content and renders in a Text-equivalent element", () => {
      const { container } = render(
        <TextOrHTML text="<strong>Bold text</strong>" autoDetect />,
      );
      expect(container.firstChild?.nodeName).toBe("P");
      expect((container.firstChild as HTMLElement)?.innerHTML).toBe(
        "<strong>Bold text</strong>",
      );
    });

    it("detects plain text and renders directly in a Text component without nested span", () => {
      render(
        <TextOrHTML text="Just plain text" autoDetect data-testid="test" />,
      );
      const element = screen.getByTestId("test");
      expect(element).toHaveClass("mobius");
      expect(element).toHaveClass(CLASS_NAME);
      expect(element).toHaveTextContent("Just plain text");
      // eslint-disable-next-line testing-library/no-node-access
      expect(element.querySelector("span")).toBeNull();
    });

    it("applies htmlClassName to the Text element for plain text", () => {
      render(
        <TextOrHTML
          text="Please enter your full name"
          htmlClassName="mobius-question__secondary-text"
          autoDetect
          data-testid="test"
        />,
      );
      const element = screen.getByTestId("test");
      expect(element).toHaveClass("mobius-question__secondary-text");
      expect(element).toHaveClass("mobius");
      expect(element).toHaveClass(CLASS_NAME);
      // eslint-disable-next-line testing-library/no-node-access
      expect(element.querySelector("span")).toBeNull();
    });

    it("renders inline HTML in non-block text with autoDetect", () => {
      render(
        <TextOrHTML
          text="This is <strong>bold</strong> text"
          autoDetect
          data-testid="test"
        />,
      );
      const element = screen.getByTestId("test");
      expect(element).toHaveClass("mobius");
      expect(element.nodeName).toBe("P");
      // eslint-disable-next-line testing-library/no-node-access
      expect(element.querySelector("strong")).toBeInTheDocument();
      expect(element).toHaveTextContent("This is bold text");
    });

    it("decodes HTML entities in text that contains no HTML tags with autoDetect", () => {
      const entities = [
        { text: "before &amp; after", expected: "before & after" },
        { text: "before &lt; after", expected: "before < after" },
        { text: "before &gt; after", expected: "before > after" },
        { text: "before &#39; after", expected: "before ' after" },
        { text: "before &quot; after", expected: 'before " after' },
      ];

      for (const { text, expected } of entities) {
        const { unmount } = render(
          <TextOrHTML text={text} autoDetect data-testid="test" />,
        );
        expect(screen.getByTestId("test")).toHaveTextContent(expected);
        unmount();
      }
    });

    it("detects HTML with leading whitespace", () => {
      const { container } = render(
        <TextOrHTML text="  <div>Indented HTML</div>" autoDetect />,
      );
      expect(container.firstChild?.nodeName).toBe("DIV");
    });

    it("treats text not starting with a tag as plain text", () => {
      render(<TextOrHTML text="Hello world" autoDetect data-testid="test" />);
      expect(screen.getByTestId("test")).toHaveClass(CLASS_NAME);
    });

    it("does not auto-detect when autoDetect is false", () => {
      const { container } = render(<TextOrHTML text="Plain text" />);
      // Without autoDetect, defaults to span with no Text wrapper
      expect(container.firstChild?.nodeName).toBe("SPAN");
      // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
      expect(container.querySelector(`.${CLASS_NAME}`)).toBeNull();
    });

    it("respects explicit htmlElementType even with autoDetect", () => {
      const { container } = render(
        <TextOrHTML
          htmlElementType="span"
          text="<div>Block HTML</div>"
          autoDetect
        />,
      );
      expect(container.firstChild?.nodeName).toBe("SPAN");
    });

    it("wraps HTML in Text when both autoDetect and textWrapper are true", () => {
      render(
        <TextOrHTML
          text="<div>Block</div>"
          htmlElementType="span"
          autoDetect
          textWrapper
          data-testid="test"
        />,
      );
      const element = screen.getByTestId("test");
      expect(element).toHaveClass(CLASS_NAME);
      // eslint-disable-next-line testing-library/no-node-access
      const span = element.querySelector("span");
      expect(span?.innerHTML).toBe("<div>Block</div>");
    });

    it("passes className to Text wrapper when autoDetect wraps plain text", () => {
      render(
        <TextOrHTML
          text="Plain text"
          autoDetect
          className="custom-class"
          data-testid="test"
        />,
      );
      const element = screen.getByTestId("test");
      expect(element).toHaveClass(CLASS_NAME);
      expect(element).toHaveClass("custom-class");
    });

    it("does not wrap in Text when autoDetect is off and textWrapper is omitted", () => {
      const { container } = render(
        <TextOrHTML text="Plain text, no wrapper" />,
      );
      expect(container.firstChild?.nodeName).toBe("SPAN");
      // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
      expect(container.querySelector(`.${CLASS_NAME}`)).toBeNull();
    });

    it("detects block-level tags appearing after leading prose", () => {
      const { container } = render(
        <TextOrHTML
          text="Estimate the total: <ul><li>Item one</li><li>Item two</li></ul>"
          autoDetect
        />,
      );
      expect(container.firstChild?.nodeName).toBe("DIV");
    });

    it("detects block-level tags appearing after leading prose with <br>", () => {
      const { container } = render(
        <TextOrHTML
          text="Estimate the total amount: <br> <ul> <li> Yourself </li> </ul>"
          autoDetect
        />,
      );
      expect(container.firstChild?.nodeName).toBe("DIV");
    });

    it("treats text with only <br> as inline, not block", () => {
      const { container } = render(
        <TextOrHTML text="Line one <br> Line two" autoDetect />,
      );
      expect(container.firstChild?.nodeName).toBe("P");
    });

    it("treats text with only inline tags as inline", () => {
      const { container } = render(
        <TextOrHTML
          text="This has <em>emphasis</em> and <strong>bold</strong>"
          autoDetect
        />,
      );
      expect(container.firstChild?.nodeName).toBe("P");
    });
  });

  // See: https://github.com/facebook/react/issues/31660
  describe("React 19 dangerouslySetInnerHTML regression", () => {
    it("should not replace innerHTML on re-render when text prop hasn't changed", async () => {
      const user = userEvent.setup();
      const TestWrapper = () => {
        const [, setCounter] = useState(0);
        return (
          <>
            <TextOrHTML text="<strong data-testid='content'>Text</strong>" />
            <button onClick={() => setCounter(c => c + 1)} data-testid="btn">
              Re-render
            </button>
          </>
        );
      };

      render(<TestWrapper />);

      const element = screen.getByTestId("content");
      element.setAttribute("data-marker", "test");

      await user.click(screen.getByTestId("btn"));

      // If innerHTML was unnecessarily replaced, the marker would be lost
      expect(screen.getByTestId("content")).toHaveAttribute(
        "data-marker",
        "test",
      );
    });

    it("should update innerHTML when text prop changes", async () => {
      const user = userEvent.setup();
      const TestWrapper = () => {
        const [text, setText] = useState(
          "<span data-testid='content'>A</span>",
        );
        return (
          <>
            <TextOrHTML text={text} />
            <button
              onClick={() => setText("<span data-testid='content'>B</span>")}
              data-testid="btn"
            >
              Update
            </button>
          </>
        );
      };

      render(<TestWrapper />);
      expect(screen.getByTestId("content")).toHaveTextContent("A");

      await user.click(screen.getByTestId("btn"));
      expect(screen.getByTestId("content")).toHaveTextContent("B");
    });
  });
});
