import * as chai from "chai";
import { chaiDom } from "../../../util/chai-dom.mjs";
import { initJSDOM } from "../../../util/jsdom.mjs";
import { ResizeObserverMock } from "../../../util/resize-observer.mjs";

let expect = chai.expect;
chai.use(chaiDom);

const html = `
  <div id="test1">
    <monster-button-bar id="bar"></monster-button-bar>
    <monster-button-bar
      id="bar-right"
      data-monster-option-layout-alignment="right"
    ></monster-button-bar>
  </div>
`;

let ButtonBar;
let ControlBar;

describe("ButtonBar", function () {
  before(function (done) {
    this.timeout(5000);
    initJSDOM()
      .then(() => {
        import("../../../../source/components/form/button-bar.mjs")
          .then((m) => {
            ButtonBar = m.ButtonBar;
            return import("../../../../source/components/form/control-bar.mjs");
          })
          .then((m) => {
            ControlBar = m.ControlBar;
            done();
          })
          .catch((e) => done(e));
      })
      .catch((e) => done(e));
  });

  beforeEach(() => {
    const mocks = document.getElementById("mocks");
    mocks.innerHTML = html;
  });

  afterEach(() => {
    const mocks = document.getElementById("mocks");
    mocks.innerHTML = "";
  });

  it("should create a button bar element", function () {
    const bar = document.getElementById("bar");
    expect(bar).to.be.instanceof(ButtonBar);
  });

  it("should inherit the generic control bar behavior", function () {
    const bar = document.getElementById("bar");

    expect(bar).to.be.instanceof(ControlBar);
    expect(bar.hideDialog).to.be.a("function");
    expect(bar.showDialog).to.be.a("function");
    expect(bar.toggleDialog).to.be.a("function");
  });

  it("should keep the popper switch hidden when no buttons are slotted", function (done) {
    const bar = document.getElementById("bar");

    setTimeout(() => {
      try {
        const switchButton = bar.shadowRoot.querySelector(
          '[data-monster-role="switch"]',
        );
        expect(switchButton).to.be.instanceof(HTMLButtonElement);
        expect(switchButton.hasAttribute("hidden")).to.be.true;
        expect(switchButton.classList.contains("hidden")).to.be.true;
        done();
      } catch (e) {
        done(e);
      }
    }, 50);
  });

  it("should default the button bar layout alignment to left", function () {
    const bar = document.getElementById("bar");
    const buttonBar = bar.shadowRoot.querySelector(
      '[data-monster-role="button-bar"]',
    );

    expect(buttonBar.getAttribute("data-monster-layout-alignment")).to.equal(
      "left",
    );
  });

  it("should allow configuring the button bar layout alignment to right", function (done) {
    const bar = document.getElementById("bar-right");

    setTimeout(() => {
      try {
        const buttonBar = bar.shadowRoot.querySelector(
          '[data-monster-role="button-bar"]',
        );

        expect(
          buttonBar.getAttribute("data-monster-layout-alignment"),
        ).to.equal("right");
        done();
      } catch (e) {
        done(e);
      }
    }, 50);
  });

  it("should render overflow popper buttons as a joined vertical group", function () {
    const cssText = ButtonBar.getCSSStyleSheet()
      .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
      .map((rule) => rule.cssText)
      .join("\n");

    expect(cssText).to.contain("slot[name=popper]");
    expect(cssText).to.contain("align-items: stretch");
    expect(cssText).to.contain("gap: 0");
    expect(cssText).to.contain("margin-top");
    expect(cssText).to.contain("var(--monster-border-width)");
  });

  it("should coalesce resize layouts and avoid unchanged layout writes", async function () {
    const OriginalResizeObserver = window.ResizeObserver;
    const originalGlobalResizeObserver = globalThis.ResizeObserver;
    const originalRequestAnimationFrame = window.requestAnimationFrame;
    const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;

    class TrackingResizeObserver extends ResizeObserverMock {
      static instances = [];

      constructor(callback) {
        super(callback);
        TrackingResizeObserver.instances.push(this);
      }
    }

    const scheduledCallbacks = [];
    const flushFrames = async () => {
      while (scheduledCallbacks.length > 0) {
        scheduledCallbacks.shift()();
        await new Promise((resolve) => setTimeout(resolve, 0));
      }
    };

    try {
      window.ResizeObserver = TrackingResizeObserver;
      globalThis.ResizeObserver = TrackingResizeObserver;
      window.requestAnimationFrame = (callback) => {
        scheduledCallbacks.push(callback);
        return scheduledCallbacks.length;
      };
      globalThis.requestAnimationFrame = window.requestAnimationFrame;

      const mocks = document.getElementById("mocks");
      mocks.innerHTML = `
        <div id="button-bar-wrapper">
          <monster-button-bar id="overflow-bar"></monster-button-bar>
        </div>
      `;

      const wrapper = document.getElementById("button-bar-wrapper");
      const bar = document.getElementById("overflow-bar");

      wrapper.style.boxSizing = "border-box";
      wrapper.style.width = "50px";
      Object.defineProperty(wrapper, "clientWidth", {
        configurable: true,
        value: 50,
      });

      const switchButton = bar.shadowRoot.querySelector(
        '[data-monster-role="switch"]',
      );
      Object.defineProperty(switchButton, "offsetWidth", {
        configurable: true,
        value: 20,
      });

      await flushFrames();

      const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
        observer.elements.includes(wrapper),
      );
      expect(resizeObserver).to.exist;

      let hiddenWriteCount = 0;
      const originalSetAttribute = switchButton.setAttribute.bind(switchButton);
      switchButton.setAttribute = (name, value) => {
        if (name === "hidden") hiddenWriteCount++;
        return originalSetAttribute(name, value);
      };

      const wrapperResizeEntry = { target: wrapper };
      resizeObserver.triggerResize([wrapperResizeEntry]);
      resizeObserver.triggerResize([wrapperResizeEntry]);

      expect(scheduledCallbacks.length).to.equal(1);
      await flushFrames();

      await new Promise((resolve) => setTimeout(resolve, 0));
      expect(scheduledCallbacks.length).to.equal(0);
      expect(hiddenWriteCount).to.equal(0);
    } finally {
      window.ResizeObserver = OriginalResizeObserver;
      globalThis.ResizeObserver = originalGlobalResizeObserver;
      window.requestAnimationFrame = originalRequestAnimationFrame;
      globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
    }
  });

  it("should ignore resize feedback caused by its own layout writes", async function () {
    const OriginalResizeObserver = window.ResizeObserver;
    const originalGlobalResizeObserver = globalThis.ResizeObserver;
    const originalRequestAnimationFrame = window.requestAnimationFrame;
    const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;

    class TrackingResizeObserver extends ResizeObserverMock {
      static instances = [];

      constructor(callback) {
        super(callback);
        TrackingResizeObserver.instances.push(this);
      }
    }

    const scheduledCallbacks = [];
    const flushOneFrame = async () => {
      const callback = scheduledCallbacks.shift();
      if (callback instanceof Function) {
        callback();
        await new Promise((resolve) => setTimeout(resolve, 0));
      }
    };
    const flushFrames = async () => {
      while (scheduledCallbacks.length > 0) {
        await flushOneFrame();
      }
    };

    try {
      window.ResizeObserver = TrackingResizeObserver;
      globalThis.ResizeObserver = TrackingResizeObserver;
      window.requestAnimationFrame = (callback) => {
        scheduledCallbacks.push(callback);
        return scheduledCallbacks.length;
      };
      globalThis.requestAnimationFrame = window.requestAnimationFrame;

      const mocks = document.getElementById("mocks");
      mocks.innerHTML = `
        <div id="feedback-bar-wrapper">
          <monster-button-bar id="feedback-bar">
            <button type="button">One</button>
            <button type="button">Two</button>
            <button type="button">Three</button>
          </monster-button-bar>
        </div>
      `;

      const wrapper = document.getElementById("feedback-bar-wrapper");
      const bar = document.getElementById("feedback-bar");

      wrapper.style.boxSizing = "border-box";
      wrapper.style.width = "80px";
      Object.defineProperty(wrapper, "clientWidth", {
        configurable: true,
        value: 80,
      });

      const buttons = Array.from(bar.querySelectorAll("button"));
      for (const button of buttons) {
        Object.defineProperty(button, "offsetWidth", {
          configurable: true,
          value: 48,
        });
        button.getBoundingClientRect = () => ({
          width: 48,
          height: 32,
          top: 0,
          left: 0,
          right: 48,
          bottom: 32,
          x: 0,
          y: 0,
        });
      }

      const switchButton = bar.shadowRoot.querySelector(
        '[data-monster-role="switch"]',
      );
      Object.defineProperty(switchButton, "offsetWidth", {
        configurable: true,
        value: 20,
      });

      await flushOneFrame();

      const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
        observer.elements.includes(wrapper),
      );
      expect(resizeObserver).to.exist;

      const suppressedFrameCount = scheduledCallbacks.length;
      resizeObserver.triggerResize([{ target: wrapper }]);
      expect(scheduledCallbacks.length).to.equal(suppressedFrameCount);

      await flushFrames();

      resizeObserver.triggerResize([{ target: wrapper }]);
      expect(scheduledCallbacks.length).to.equal(1);

      await flushFrames();
      expect(scheduledCallbacks.length).to.equal(0);
    } finally {
      window.ResizeObserver = OriginalResizeObserver;
      globalThis.ResizeObserver = originalGlobalResizeObserver;
      window.requestAnimationFrame = originalRequestAnimationFrame;
      globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
    }
  });
});
