import { assert } from "./assert";

const util = require("util");

export function dispatchPointerEvent(type: string, target: HTMLElement) {
  const rect = target.getBoundingClientRect();

  const clientX = rect.left + rect.width / 2;
  const clientY = rect.top + rect.height / 2;

  const event = new MouseEvent(type, {
    bubbles: true,
    clientX,
    clientY,
    buttons: 1,
  });
  Object.defineProperties(event, {
    pageX: {
      get() {
        return clientX;
      },
    },
    pageY: {
      get() {
        return clientY;
      },
    },
    isPrimary: {
      value: true,
    },
  });

  target.dispatchEvent(event);
}

export function expectToBeCloseToArray(
  actualNumbers: number[],
  expectedNumbers: number[]
) {
  expect(actualNumbers.length).toBe(expectedNumbers.length);

  try {
    actualNumbers.forEach((actualNumber, index) => {
      const expectedNumber = expectedNumbers[index];
      assert(expectedNumber != null, `Expected number not found`);

      expect(actualNumber).toBeCloseTo(expectedNumber, 1);
    });
  } catch (error) {
    expect(actualNumbers).toEqual(expectedNumbers);
  }
}

export function mockBoundingClientRect(
  element: HTMLElement,
  rect: {
    height: number;
    width: number;
    x: number;
    y: number;
  }
) {
  const { height, width, x, y } = rect;

  Object.defineProperty(element, "getBoundingClientRect", {
    configurable: true,
    value: () =>
      ({
        bottom: y + height,
        height,
        left: x,
        right: x + width,
        toJSON() {
          return "";
        },
        top: y,
        width,
        x,
        y,
      }) satisfies DOMRect,
  });
}

export function mockPanelGroupOffsetWidthAndHeight(
  mockWidth = 1_000,
  mockHeight = 1_000
) {
  const offsetHeightPropertyDescriptor = Object.getOwnPropertyDescriptor(
    HTMLElement.prototype,
    "offsetHeight"
  );

  const offsetWidthPropertyDescriptor = Object.getOwnPropertyDescriptor(
    HTMLElement.prototype,
    "offsetWidth"
  );

  Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
    configurable: true,
    get: function () {
      if (this.hasAttribute("data-resize-handle")) {
        return 0;
      } else if (this.hasAttribute("data-panel-group")) {
        return mockHeight;
      }
    },
  });

  Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
    configurable: true,
    get: function () {
      if (this.hasAttribute("data-resize-handle")) {
        return 0;
      } else if (this.hasAttribute("data-panel-group")) {
        return mockWidth;
      }
    },
  });

  return function uninstallMocks() {
    if (offsetHeightPropertyDescriptor) {
      Object.defineProperty(
        HTMLElement.prototype,
        "offsetHeight",
        offsetHeightPropertyDescriptor
      );
    }

    if (offsetWidthPropertyDescriptor) {
      Object.defineProperty(
        HTMLElement.prototype,
        "offsetWidth",
        offsetWidthPropertyDescriptor
      );
    }
  };
}

export function verifyAttribute(
  element: HTMLElement,
  attributeName: string,
  expectedValue: string | null
) {
  const actualValue = element.getAttribute(attributeName);
  expect(actualValue).toBe(expectedValue);
}

export function verifyExpandedPanelGroupLayout(
  actualLayout: number[],
  expectedLayout: number[]
) {
  expect(actualLayout).toEqual(expectedLayout);
}

export function verifyExpectedWarnings(
  callback: Function,
  ...expectedMessages: string[]
) {
  const consoleSpy = (format: any, ...args: any[]) => {
    const message = util.format(format, ...args);

    for (let index = 0; index < expectedMessages.length; index++) {
      const expectedMessage = expectedMessages[index];
      if (message.includes(expectedMessage)) {
        expectedMessages.splice(index, 1);
        return;
      }
    }

    if (expectedMessages.length === 0) {
      throw new Error(`Unexpected message recorded:\n\n${message}`);
    }
  };

  const originalError = console.error;
  const originalWarn = console.warn;

  console.error = consoleSpy;
  console.warn = consoleSpy;

  let caughtError;
  let didCatch = false;
  try {
    callback();
  } catch (error) {
    caughtError = error;
    didCatch = true;
  } finally {
    console.error = originalError;
    console.warn = originalWarn;

    if (didCatch) {
      throw caughtError;
    }

    // Any remaining messages indicate a failed expectations.
    if (expectedMessages.length > 0) {
      throw Error(
        `Expected message(s) not recorded:\n\n${expectedMessages.join("\n")}`
      );
    }

    return { pass: true };
  }
}
