import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen } from "@testing-library/dom";
import { userEvent } from "@testing-library/user-event";
import {
  ElementWithUtils,
  qsr,
  onDOMReady,
  __testing,
} from "../../src/ts/utils/dom";
const resetReady = __testing.resetReady;

describe("dom", () => {
  describe("ElementWithUtils", () => {
    describe("onChild", () => {
      const handler = vi.fn();

      function registerOnChild(
        event: string,
        selector: string,
        options?: {
          parent?: ElementWithUtils;
        },
      ): void {
        const parent = options?.parent ?? qsr("#parent");
        parent?.onChild(event, selector, (e) =>
          handler({
            target: e.target,
            childTarget: e.childTarget,
            currentTarget: e.currentTarget,
          }),
        );
      }

      beforeEach(() => {
        handler.mockClear();

        document.body.innerHTML = "";
        const root = document.createElement("div");

        root.innerHTML = `
          <div id="parent" data-testid="parent">
            <section id="decoy">
            <div id="mid1" data-testid="mid1" class="middle">
                <div id="inner1" class="inner">test</div>
                <div id="inner2" data-testid="inner2" class="inner">
                  test
                  <button id="button" data-testid="button">
                    click me 
                    <i id="icon" data-testid="icon">test</i>
                  </button>
                </div>
            </div>
            <div id="mid2" class="middle">
                <div id="inner3" class="inner">test</div>
                <div id="inner4" class="inner">test</div>
            </div>
            </section>
        </div>
        `;
        document.body.appendChild(root);
      });

      it("should not fire when parent element is clicked", async () => {
        //GIVEN
        registerOnChild("click", "div");

        //WHEN
        await userEvent.click(screen.getByTestId("parent"));

        //THEN
        expect(handler).not.toHaveBeenCalled();
      });

      it("should not fire when selector doesnt match", async () => {
        //GIVEN
        const buttonEl = qsr("#button");
        registerOnChild("click", "div", { parent: buttonEl });

        //WHEN
        await userEvent.click(screen.getByTestId("icon"));

        //THEN
        expect(handler).not.toHaveBeenCalled();
      });

      it("should fire when selector is clicked", async () => {
        //GIVEN
        registerOnChild("click", "div");

        //WHEN
        const clickTarget = screen.getByTestId("mid1");
        await userEvent.click(clickTarget);

        //THEN
        expect(handler).toHaveBeenCalledWith(
          expect.objectContaining({
            target: clickTarget,
            childTarget: clickTarget,
            currentTarget: screen.getByTestId("parent"),
          }),
        );
      });

      it("should fire when child of selector is clicked", async () => {
        //GIVEN
        registerOnChild("click", "div.middle");

        //WHEN
        const selectorTarget = screen.getByTestId("mid1");
        const clickTarget = screen.getByTestId("button");
        await userEvent.click(clickTarget);

        //THEN
        expect(handler).toHaveBeenCalledWith(
          expect.objectContaining({
            target: clickTarget,
            childTarget: selectorTarget,
            currentTarget: screen.getByTestId("parent"),
          }),
        );
      });

      it("should fire on each element matching the selector from the child up to the parent", async () => {
        //GIVEN
        registerOnChild("click", "div.middle, div.inner");

        //WHEN
        let clickTarget = screen.getByTestId("button");
        await userEvent.click(clickTarget);

        //THEN
        expect(handler).toHaveBeenCalledTimes(2);

        //First call is for childTarget inner2 (grand child of parent)
        expect(handler).toHaveBeenNthCalledWith(
          1,
          expect.objectContaining({
            target: clickTarget,
            childTarget: screen.getByTestId("inner2"),
            currentTarget: screen.getByTestId("parent"),
          }),
        );

        //Second call is for childTarget mid1 (child of parent)
        expect(handler).toHaveBeenNthCalledWith(
          2,
          expect.objectContaining({
            target: clickTarget,
            childTarget: screen.getByTestId("mid1"),
            currentTarget: screen.getByTestId("parent"),
          }),
        );

        //WHEN click on mid1 handler is only called one time
        handler.mockClear();
        clickTarget = screen.getByTestId("mid1");
        await userEvent.click(clickTarget);

        //THEN
        expect(handler).toHaveBeenCalledTimes(1);

        expect(handler).toHaveBeenCalledWith(
          expect.objectContaining({
            target: clickTarget,
            childTarget: clickTarget,
            currentTarget: screen.getByTestId("parent"),
          }),
        );
      });
    });
  });
  describe("onDOMReady", () => {
    beforeEach(() => {
      document.body.innerHTML = "";
      resetReady();
      vi.useFakeTimers();
    });

    function dispatchEvent(event: "DOMContextLoaded" | "load"): void {
      if (event === "DOMContextLoaded") {
        document.dispatchEvent(new Event("DOMContentLoaded"));
      } else {
        window.dispatchEvent(new Event("load"));
      }

      vi.runAllTimers();
    }

    it("executes callbacks when DOMContentLoaded fires", () => {
      const spy = vi.fn();
      onDOMReady(spy);
      expect(spy).not.toHaveBeenCalled();

      dispatchEvent("DOMContextLoaded");

      expect(spy).toHaveBeenCalledOnce();
    });

    it("executes callbacks added before ready in order", () => {
      const calls: number[] = [];
      onDOMReady(() => calls.push(1));
      onDOMReady(() => calls.push(2));

      dispatchEvent("DOMContextLoaded");

      expect(calls).toEqual([1, 2]);
    });

    it("executes callbacks asynchronously when DOM is already ready", () => {
      const spy = vi.fn();

      Object.defineProperty(document, "readyState", {
        value: "complete",
        configurable: true,
      });

      onDOMReady(spy);

      expect(spy).not.toHaveBeenCalled();

      vi.runAllTimers();

      expect(spy).toHaveBeenCalledOnce();
    });

    it("executes callbacks added after ready asynchronously", () => {
      const calls: string[] = [];
      onDOMReady(() => calls.push("ready"));

      dispatchEvent("DOMContextLoaded");

      onDOMReady(() => calls.push("late"));

      expect(calls).toEqual(["ready"]);

      vi.runAllTimers();

      expect(calls).toEqual(["ready", "late"]);
    });

    it("executes callbacks added during ready execution", () => {
      const calls: number[] = [];

      onDOMReady(() => {
        calls.push(1);
        onDOMReady(() => calls.push(3));
      });

      onDOMReady(() => calls.push(2));

      dispatchEvent("DOMContextLoaded");

      expect(calls).toEqual([1, 2, 3]);
    });

    it("does not execute ready callbacks more than once", () => {
      const spy = vi.fn();

      onDOMReady(spy);

      dispatchEvent("DOMContextLoaded");
      dispatchEvent("load");

      expect(spy).toHaveBeenCalledOnce();
    });

    it("falls back to window load event if DOMContentLoaded does not fire", () => {
      const spy = vi.fn();

      onDOMReady(spy);

      dispatchEvent("load");

      expect(spy).toHaveBeenCalledOnce();
    });
  });
});
