import "chai/chai.js";
import { expect } from "chai";
import {
  fireEvent,
  getByLabelText,
  getByRole,
  getByText,
  queryAllByLabelText,
  queryByLabelText,
  queryByText,
} from "@testing-library/dom";
import "mocha/mocha.js";
import { inflictBoreDOM, webComponent } from "../src/index";

function renderHTML(html: string) {
  const main = document.querySelector("main");
  if (!main) throw new Error("No <main> found!");
  main.innerHTML = html;
  return main;
}

async function frame(): Promise<number> {
  return new Promise((resolve) => {
    requestAnimationFrame((t) => resolve(t));
  });
}

async function renderHTMLFrame(html: string): Promise<HTMLElement> {
  const main = document.querySelector("main");
  if (!main) throw new Error("No <main> found!");
  main.innerHTML = html;
  return (new Promise((resolve) => {
    requestAnimationFrame(() => {
      resolve(main);
    });
  }));
}

export default function () {
  describe("DOM", () => {
    beforeEach(function () {
      const main = document.querySelector("main");
      if (!main) return;
      main.innerHTML = "";
    });

    describe("Simple component", () => {
      it("should register the <template> data-component tag", async () => {
        const container = renderHTML(
          `<template data-component="simple-component"></template>`,
        );
        inflictBoreDOM();

        const ctor = customElements.get("simple-component");

        expect(ctor).not.to.be.undefined;
        if (!ctor) throw new Error("Undefined tag");
        expect(new ctor()).to.be.an.instanceof(HTMLElement);
      });

      it("should not register the <template> data-component tag if it is invalid", async () => {
        const container = renderHTML(
          `<template data-component="nonvalid"></template>`,
        );
        inflictBoreDOM();

        const ctor = customElements.get("nonvalid");

        expect(ctor).to.be.undefined;
      });

      it("should render the html of the custom element", async () => {
        const container = renderHTML(`
          <simple-component2></simple-component2>
          <template data-component="simple-component2"><p>This is some random HTML</p></template>
        `);
        inflictBoreDOM();

        const elem = getByText(container, "This is some random HTML");
        expect(elem).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should render in shadow root the html of the corresponding <template> tag when it has shadowmode set", async () => {
        const container = renderHTML(`
          <simple-component3></simple-component3>
          <template data-component="simple-component3" shadowrootmode="open"><p>Test</p></template>
        `);
        inflictBoreDOM();

        const elem = getByText(
          (container.firstElementChild as any).shadowRoot,
          "Test",
        );
        expect(elem).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should apply the aria attributes from the <template> to the tag of the custom element", async () => {
        const container = renderHTML(`
          <simple-component4></simple-component4>
          <template data-component="simple-component4" data-aria-label="Some Label"><p>Something</p></template>
        `);
        inflictBoreDOM();

        const elem = getByLabelText(container, "Some Label");
        expect(elem).to.be.an.instanceof(HTMLElement);
        expect(elem.tagName).to.equal("SIMPLE-COMPONENT4");
        expect(elem.firstChild).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should apply the role attribute from the <template> to the tag of the custom element", async () => {
        const container = renderHTML(`
          <simple-component5></simple-component5>
          <template data-component="simple-component5" data-role="banner"><p>Something</p></template>
        `);
        inflictBoreDOM();

        const elem = getByRole(container, "banner");
        expect(elem).to.be.an.instanceof(HTMLElement);
        expect(elem.tagName).to.equal("SIMPLE-COMPONENT5");
        expect(elem.firstChild).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should allow the slots default behaviour", async () => {
        const container = await renderHTMLFrame(`
          <slotted-component1>
            <span slot="my-text">Let's have some different text!</span>
          </slotted-component1>

          <template data-component="slotted-component1" shadowrootmode="open">
            <p><slot name="my-text">My default text</slot></p>
          </template>
        `);

        await inflictBoreDOM(); // replacing slots requires "shadowrootmode" to be set
        const elem = getByText(
          container,
          "Let's have some different text!",
        );
        expect(elem).to.be.an.instanceof(HTMLElement);

        const shouldNotExist = queryByText(
          container,
          "My default text",
        );
        expect(shouldNotExist).to.be.null;
      });
    });
    describe("Simple component events", () => {
      it("should set a data-event-dispatches on the web component once the custom event is registered", () => {
        const container = renderHTML(`
          <eventful-component1></eventful-component1>
          <template data-component="eventful-component1"><button onclick="dispatch('clickme')">Click me</button></template>
        `);
        inflictBoreDOM();

        const elem = container.querySelector(
          "[data-onclick-dispatches]",
        ) as HTMLElement;
        expect(elem).to.be.an.instanceof(HTMLElement);
        expect(elem.dataset.onclickDispatches).to.eql("clickme");
      });

      it("should dispatch a custom event with the provided name in the dispatch function", async (done) => {
        const container = renderHTML(`
          <eventful-component2></eventful-component2>
          <template data-component="eventful-component2"><button onclick="dispatch('clickme')">Click me</button></template>
        `);
        inflictBoreDOM();

        addEventListener("clickme", (e: any) => {
          expect(e.detail.event).not.to.be.undefined;
          expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
          if (!(e.detail.event.target instanceof HTMLElement)) {
            throw new Error("Event target not an html element");
          }
          expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
            "button",
          );
          done();
        });

        const elem = getByText(
          container,
          "Click me",
        );
        fireEvent.click(elem);
      });

      it("should dispatch more than one custom event when more than one string is in the dispatch function", async (done) => {
        const container = renderHTML(`
          <eventful-component3></eventful-component3>
          <template data-component="eventful-component3"><button onclick="dispatch('clickyou', 'clickthem')">Click me</button></template>
        `);
        inflictBoreDOM();

        let triggeredEvents: string[] = [];
        addEventListener("clickthem", (e: any) => {
          expect(e.detail.event).not.to.be.undefined;
          expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
          if (!(e.detail.event.target instanceof HTMLElement)) {
            throw new Error("Event target not an html element");
          }
          expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
            "button",
          );

          triggeredEvents.push("clickthem");
          if (triggeredEvents.includes("clickyou")) {
            done();
          }
        });

        addEventListener("clickyou", (e: any) => {
          expect(e.detail.event).not.to.be.undefined;
          expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
          if (!(e.detail.event.target instanceof HTMLElement)) {
            throw new Error("Event target not an html element");
          }
          expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
            "button",
          );

          triggeredEvents.push("clickyou");
          if (triggeredEvents.includes("clickthem")) {
            done();
          }
        });

        const elem = getByText(
          container,
          "Click me",
        );
        // One click, should trigger two custom events
        fireEvent.click(elem);
      });
    });

    describe("Component with <script> code", () => {
      it("should load the associated JS and run the render function", async () => {
        // The following code is accompanied by the `stateful-component1.js` file.
        const container = renderHTML(`
          <stateful-component1></stateful-component1>

          <template data-component="stateful-component1">
            <p>Stateful component 1</p>
          </template>

          <script src="/stateful-component1.js"></script>
        `);
        // The `stateful-component1.js` should be automatically imported dynamically and
        // its render function called.
        await inflictBoreDOM();

        const elem = getByText(
          container,
          "Render",
        );
        expect(elem).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should pass refs through an object in init", async () => {
        // The following code is accompanied by the `stateful-component2.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component2></stateful-component2>

          <template data-component="stateful-component2">
            <p>Some ref:
              <span data-ref="something"> </span> </p>
            <!-- ^ should be available as options.refs.something in the init function -->
          </template>

          <script src="/stateful-component2.js"></script>
        `);

        await inflictBoreDOM(); // Runs the code in `stateful-component2.js`

        const elem = getByText(
          container,
          "Something ref innerText updated",
        );
        expect(elem).to.be.an.instanceof(HTMLSpanElement);
      });

      it("should throw an error when an undefined ref is being accessed", async () => {
        // The following code is accompanied by the `stateful-component3.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component3></stateful-component3>

          <template data-component="stateful-component3"></template>

          <script src="/stateful-component3.js"></script>
        `);

        try {
          await inflictBoreDOM(); // Runs the code in `stateful-component3.js`
        } catch (e) {
          expect((e as Error).message).to.be.a.string(
            'Ref "somethingThatDoesNotExist" not found in <STATEFUL-COMPONENT3>',
          );
        }
      });

      it("should be able to get slots through the `slots` object property in the render function", async () => {
        // The following code is accompanied by the `stateful-component4.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component4></stateful-component4>

          <template data-component="stateful-component4">
            <p>Something can be placed below:</p>
            <slot name="some-slot"></slot>
            <!-- ^ should be available as options.slots["some-slot"] in the render function -->
          </template>

          <script src="/stateful-component4.js"></script>

          <template data-component="stateful-component4b">
            <p>This component will be placed in the slot by the .js code</p>
          </template>
        `);

        await inflictBoreDOM(); // Runs the code in `stateful-component4.js`

        const elem = getByText(
          container,
          "This component will be placed in the slot by the .js code",
        );
        expect(elem).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should be able to set slots through the `slots` object property and replace the slot element", async () => {
        // The following code is accompanied by the `stateful-component5.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component5></stateful-component5>

          <template data-component="stateful-component5">
            <p>Something can be placed below:</p>
            <slot name="some-slot">This will be replaced</slot>
            <!-- ^ should be available to be replaced by setting options.slots["some-slot"] in the render function -->
          </template>

          <script src="/stateful-component5.js"></script>
        `);

        await inflictBoreDOM(); // Runs the code in `stateful-component5.js`

        const replaced = queryByText(
          container,
          "This will be replaced",
        );
        expect(replaced).to.be.null;

        const elem = getByText(
          container,
          "Text in a paragraph that replaced the slot",
        );
        expect(elem).to.be.an.instanceof(HTMLParagraphElement);
      });

      it("should place the slot name in a data attribute of the element that replaces it", async () => {
        // The following code is accompanied by the `stateful-component5.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component5></stateful-component5>

          <template data-component="stateful-component5">
            <p>Something can be placed below:</p>
            <slot name="some-slot">This will be replaced</slot>
            <!-- ^ should be available to be replaced by setting options.slots["some-slot"] in the render function -->
          </template>

          <script src="/stateful-component5.js"></script>
        `);

        await inflictBoreDOM(); // Runs the code in `stateful-component5.js`

        const elem = getByText(
          container,
          "Text in a paragraph that replaced the slot",
        );
        expect(elem.dataset.slot).to.be.string(
          "some-slot",
          "Should have a `data-slot='slot-name' attribute`",
        );
      });

      it("should allow script code to be defined in the `inflictBoreDOM()` function", async () => {
        const container = renderHTML(`
          <inline-component1></inline-component1>

          <template data-component="inline-component1">
            <p>Stateful inline component 1</p>
          </template>

          <!-- code will be set in inflictBoreDOM -->
        `);

        await inflictBoreDOM(undefined, {
          "inline-component1": webComponent(() => ({ self }) => {
            self.innerHTML = "Inline code run";
          }),
        });

        const elem = getByText(
          container,
          "Inline code run",
        );
        expect(elem).to.be.an.instanceof(HTMLElement);
        expect(elem.tagName).to.be.equals("INLINE-COMPONENT1");
      });

      it("should initialize all instances of the same component", async () => {
        const container = await renderHTMLFrame(`
          <multi-instance-component></multi-instance-component>
          <multi-instance-component></multi-instance-component>

          <template data-component="multi-instance-component">
            <p>Multi instance component</p>
          </template>

          <script src="/multi-instance-component.js"></script>
        `);

        await inflictBoreDOM();

        const instances = Array.from(
          container.querySelectorAll("multi-instance-component"),
        );

        expect(instances.length).to.equal(2);
        expect(instances[0]).to.be.an.instanceof(HTMLElement);
        expect(instances[1]).to.be.an.instanceof(HTMLElement);

        // Both instances should have been initialized with their index
        expect(instances[0].getAttribute("data-index")).to.equal("0");
        expect(instances[1].getAttribute("data-index")).to.equal("1");
      });
    });

    describe("Event handlers in scripts", () => {
      it(
        "should handle custom events with the provided 'on' function",
        function (done) {
          (async () => {
            // The following code is accompanied by the `stateful-component5.js` file.
            const container = await renderHTMLFrame(`
          <on-event-component1></on-event-component1>

          <template data-component="on-event-component1">
            <button onclick="dispatch('someCustomEventOnClick')">Click here to dispatch</butbbon>
          </template>
          <script src="/on-event-component1.js"></script>
        `);

            const state = { onDone: done };

            await inflictBoreDOM(state);

            const elem = getByText(
              container,
              "Click here to dispatch",
            );

            // One click, should trigger the custom event, and call the registered callbackes
            // provided to the 'on' function (see 'on-event-component1.js')
            fireEvent.click(elem);
          })();
        },
      );

      it(
        "should be able to update the state and automatically render in the provided 'on' function",
        async () => {
          // The following code is accompanied by the `stateful-component5.js` file.
          const container = await renderHTMLFrame(`
          <on-event-component2></on-event-component2>

          <template data-component="on-event-component2">
            <p data-ref="label">Value</p>
            <button onclick="dispatch('incrementClick')">Increment</button>
          </template>
          <script src="/on-event-component2.js"></script>
        `);

          const state = { value: 0 };

          await inflictBoreDOM(state);

          // Label should be "0", because the "value" attribute is being set on render:
          const labelElem = getByText(
            container,
            "0",
          );

          const btn = getByText(
            container,
            "Increment",
          );

          // One click, should trigger the custom event, and call the registered callbackes
          // provided to the 'on' function (see 'on-event-component1.js')
          fireEvent.click(btn);

          await frame();

          const newLabelElem = getByText(
            container,
            "1",
          );
          expect(newLabelElem.innerText).to.be.string("1");
        },
      );
    });

    describe("State in component <script> code", () => {
      it("should pass the provided state ", async () => {
        // The following code is accompanied by the `stateful-component6.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component6></stateful-component6>

          <template data-component="stateful-component6">
            <p>Initial state is: <span data-ref="container"></span></p>
          </template>

          <script src="/stateful-component6.js"></script>
        `);

        await inflictBoreDOM({ content: { value: "Initial state" } }); // Runs the code in `stateful-component6.js`

        const elem = getByText(
          container,
          "Initial state",
        );
        expect(elem).to.be.an.instanceof(HTMLSpanElement);
      });

      it("should re-render when the provided state has changed", async () => {
        // The following code is accompanied by the `stateful-component6.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component6></stateful-component6>

          <template data-component="stateful-component6">
            <p>Initial state is: <span data-ref="container"></span></p>
          </template>

          <script src="/stateful-component6.js"></script>
        `);

        const state = { content: { value: "Initial state" } };
        await inflictBoreDOM(state); // Runs the code in `stateful-component6.js`

        // Update the state:
        state.content.value = "This is new content";

        await frame();

        const elem = getByText(
          container,
          "This is new content",
        );
        expect(elem).to.be.an.instanceof(HTMLSpanElement);
      });

      it("should re-render when an array changed in the provided state", async () => {
        // The following code is accompanied by the `stateful-component6.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component7></stateful-component7>

          <template data-component="stateful-component7">
            <p>Initial state is: <span data-ref="container"></span></p>
          </template>

          <script src="/stateful-component7.js"></script>
        `);

        const state = { content: { value: ["Initial state"] } };
        await inflictBoreDOM(state); // Runs the code in `stateful-component7.js`

        // Update the state:
        state.content.value[0] = "This is new content";

        await frame();

        const elem = getByText(
          container,
          "This is new content",
        );
        expect(elem).to.be.an.instanceof(HTMLSpanElement);
      });

      it("should re-render when an array changed in an event handler", async () => {
        // The following code is accompanied by the `stateful-component6.js` file.
        const container = await renderHTMLFrame(`
          <stateful-component8></stateful-component8>

          <template data-component="stateful-component8">
            <button onclick="dispatch('update')">Click to update</button>
            <p>Initial state is: <span data-ref="container"></span></p>
          </template>

          <script src="/stateful-component8.js"></script>
        `);

        const state = { content: { value: ["Initial state"] } };
        await inflictBoreDOM(state); // Runs the code in `stateful-component8.js`

        const btn = getByText(
          container,
          "Click to update",
        );

        fireEvent.click(btn);

        await frame();

        const elem = getByText(
          container,
          "This is new content",
        );
        expect(elem).to.be.an.instanceof(HTMLSpanElement);
      });
    });

    describe("Lists of components in <script> code", () => {
      it("should be able to dynamically create a component with a detail object", async () => {
        // The following code is accompanied by the `list-component1.js` file.
        const container = await renderHTMLFrame(`
          <list-component1></list-component1>

          <template data-component="list-component1">
            <p>Below will be added a dynamic component</p>
            <ol>
            </ol>
          </template>
          <script src="/list-component1.js"></script>

          <template data-component="list-item1">
            <li></li>
          </template>
          <script src="/list-item1.js"></script>
        `);

        await frame();

        await inflictBoreDOM({ content: { items: ["some item"] } }); // Runs the code in `list-component1.js`

        const elem = getByText(
          container,
          "some item",
        );
        expect(elem).to.be.an.instanceof(HTMLElement);
      });

      it("should dynamically create multiple components", async () => {
        // The following code is accompanied by the `list-component1.js` file.
        // This is the same as the previous test
        const container = await renderHTMLFrame(`
          <list-component1></list-component1>

          <template data-component="list-component1">
            <p>Below will be added a dynamic component</p>
            <ol>
            </ol>
          </template>
          <script src="/list-component1.js"></script>

          <template data-component="list-item1">
            <li></li>
          </template>
          <script src="/list-item1.js"></script>
        `);

        await frame();

        // In this test, pass multiple items in the array
        await inflictBoreDOM({
          content: { items: ["item A", "item B", "item C"] },
        });
        //    ^ Runs the code in `list-component1.js`
        const elem1 = getByText(
          container,
          "item A",
        );
        const elem2 = getByText(
          container,
          "item B",
        );
        const elem3 = getByText(
          container,
          "item C",
        );
        expect(elem1).to.be.an.instanceof(HTMLElement);
        expect(elem2).to.be.an.instanceof(HTMLElement);
        expect(elem3).to.be.an.instanceof(HTMLElement);
      });
    });
  });
}
