let id = 0;
const createId = () => {
  return ++id;
};

const callbacks = Object.create(null);
const registerPromise = () => {
  const id = createId();
  const {
    resolve,
    promise
  } = Promise.withResolvers();
  callbacks[id] = resolve;
  return {
    id,
    promise
  };
};
const resolve = (id, value) => {
  const fn = callbacks[id];
  delete callbacks[id];
  fn(value);
};

const commands = Object.create(null);
const register = commandMap => {
  Object.assign(commands, commandMap);
};
const getCommand = key => {
  return commands[key];
};
const execute = (command, ...args) => {
  const fn = getCommand(command);
  if (!fn) {
    throw new Error(`command not found ${command}`);
  }
  return fn(...args);
};

let port = undefined;
const set = value => {
  port = value;
};
const get = () => {
  return port;
};

const apiFactory = commandMap => {
  register(commandMap);
  return {
    async invoke(method, ...params) {
      const {
        id,
        promise
      } = registerPromise();
      const port = get();
      port.postMessage({
        jsonrpc: '2.0',
        id,
        method,
        params
      });
      const response = await promise;
      // @ts-ignore
      if (response && 'error' in response) {
        // @ts-ignore
        throw new Error(response.error.message);
      }
      // TODO unwrap jsonrpc result
      return response;
    }
  };
};

const htmlElements = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdo', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'];

const isElement = selector => {
  return htmlElements.includes(selector);
};

const querySelectorByCss = selector => {
  return [...document.querySelectorAll(selector)];
};

const querySelectorByText = (root, text) => {
  let node;
  const elements = [];
  const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
  while (node = walk.nextNode()) {
    if (node.nodeValue === text) {
      elements.push(node.parentNode);
    }
  }
  return elements;
};

const querySelector = selector => {
  if (typeof selector !== 'string') {
    throw new TypeError('selector must be of type string');
  }
  if (selector.startsWith('text=')) {
    return querySelectorByText(document.body, selector.slice('text='.length));
  }
  if (selector.includes('text=')) {
    const index = selector.indexOf('text=');
    const elements = querySelectorByCss(selector.slice(0, index));
    const text = selector.slice(index + 'text='.length);
    return elements.flatMap(element => {
      return querySelectorByText(element, text);
    });
    // for(const element of elements)
  }
  if (selector.startsWith('.')) {
    return querySelectorByCss(selector);
  }
  if (selector.startsWith('#')) {
    return querySelectorByCss(selector);
  }
  if (selector.startsWith('[data') || selector.startsWith('[title') || selector.startsWith('[name')) {
    return querySelectorByCss(selector);
  }
  if (isElement(selector)) {
    return querySelectorByCss(selector);
  }
  throw new Error(`unsupported selector: ${selector}`);
};

const querySelectorWithOptions = (selector, {
  nth = -1,
  hasText = ''
} = {}) => {
  let elements = querySelector(selector);
  if (hasText) {
    elements = elements.filter(element => element.textContent === hasText);
  }
  if (elements.length === 0) {
    return undefined;
  }
  if (elements.length === 1) {
    const element = elements[0];
    return element;
  }
  if (nth === -1) {
    throw new Error(`too many matching elements for ${selector}, matching ${elements.length}`);
  }
  const element = elements[nth];
  if (!element) {
    throw new Error(`selector not found: ${selector}`);
  }
  return element;
};

const toHaveText$1 = locator => {
  const element = querySelectorWithOptions(locator._selector, {
    nth: locator._nth,
    hasText: locator._hasText
  });
  if (!element) {
    return {
      wasFound: false,
      actual: ''
    };
  }
  return {
    wasFound: true,
    actual: element.textContent
  };
};
const toHaveAttribute$1 = (locator, {
  key,
  value
}) => {
  const element = querySelectorWithOptions(locator._selector, {
    nth: locator._nth,
    hasText: locator._hasText
  });
  if (!element) {
    return {
      wasFound: false,
      actual: ''
    };
  }
  const actual = element.getAttribute(key);
  return {
    wasFound: false,
    actual
  };
};
const toHaveCount = (locator, {
  count
}) => {
  const elements = querySelector(locator._selector);
  const actualCount = elements.length;
  return {
    actual: actualCount
  };
};
const stringifyElement = element => {
  if (element.id) {
    return `#${element.id}`;
  }
  if (element.className) {
    return `.${element.className}`;
  }
  if (element === document.body) {
    return 'document.body';
  }
  return element.tagName;
};
const toBeFocused$1 = locator => {
  const {
    activeElement
  } = document;
  const stringifiedActiveElement = stringifyElement(activeElement);
  return {
    actual: stringifiedActiveElement
  };
};
const toHaveClass$1 = (locator, {
  className
}) => {
  const [element] = querySelector(locator._selector);
  if (!element) {
    return {
      wasFound: false,
      actual: ''
    };
  }
  return {
    wasFound: true,
    actual: className
  };
};
const toHaveId$1 = locator => {
  const [element] = querySelector(locator._selector);
  if (!element) {
    return {
      wasFound: false,
      actual: ''
    };
  }
  return {
    wasFound: true,
    actual: element.id
  };
};
const toHaveCss$1 = (locator, {
  key
}) => {
  const [element] = querySelector(locator._selector);
  if (!element) {
    return {
      wasFound: false,
      actual: ''
    };
  }
  const style = getComputedStyle(element);
  const actual = style[key];
  return {
    wasFound: true,
    actual
  };
};

var ConditionValues = /*#__PURE__*/Object.freeze({
  __proto__: null,
  toBeFocused: toBeFocused$1,
  toHaveAttribute: toHaveAttribute$1,
  toHaveClass: toHaveClass$1,
  toHaveCount: toHaveCount,
  toHaveCss: toHaveCss$1,
  toHaveId: toHaveId$1,
  toHaveText: toHaveText$1
});

const checkConditionError = (fnName, ...params) => {
  const fn = ConditionValues[fnName];
  return fn(...params);
};

const toBeVisible = element => {
  // @ts-ignore
  if (typeof element.isVisible === 'function') {
    // @ts-ignore
    return element.isVisible();
  }
  return element.isConnected;
};
const toHaveValue = (element, {
  value
}) => {
  return element.value === value;
};
const toHaveText = (element, {
  text
}) => {
  return element.textContent === text;
};
const toContainText = (element, {
  text
}) => {
  // @ts-ignore
  return element.textContent.includes(text);
};
const toHaveAttribute = (element, {
  key,
  value
}) => {
  const attribute = element.getAttribute(key);
  return attribute === value;
};
const toBeFocused = element => {
  return element === document.activeElement;
};
const toHaveClass = (element, {
  className
}) => {
  return element.classList.contains(className);
};
const toHaveId = (element, {
  id
}) => {
  return element.id === id;
};
const toHaveCss = (element, {
  key,
  value
}) => {
  const style = getComputedStyle(element);
  const actualValue = style[key];
  if (value instanceof RegExp) {
    return value.test(actualValue);
  }
  return actualValue === value;
};

var SingleElementConditions = /*#__PURE__*/Object.freeze({
  __proto__: null,
  toBeFocused: toBeFocused,
  toBeVisible: toBeVisible,
  toContainText: toContainText,
  toHaveAttribute: toHaveAttribute,
  toHaveClass: toHaveClass,
  toHaveCss: toHaveCss,
  toHaveId: toHaveId,
  toHaveText: toHaveText,
  toHaveValue: toHaveValue
});

const getTimeStamp = () => {
  return performance.now();
};

const waitForMutation = async maxDelay => {
  const disposables = [];
  const {
    resolve: resolve1,
    promise: promise1
  } = Promise.withResolvers();
  const {
    resolve: resolve2,
    promise: promise2
  } = Promise.withResolvers();
  const timeout = setTimeout(resolve1, maxDelay);
  disposables.push(() => {
    clearTimeout(timeout);
  });
  const callback = mutations => {
    resolve2(undefined);
  };
  const observer = new MutationObserver(callback);
  observer.observe(document.body, {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
    characterDataOldValue: true
  });
  disposables.push(() => {
    observer.disconnect();
  });
  await Promise.race([promise1, promise2]);
  for (const disposable of disposables) {
    disposable();
  }
};

const maxTimeout$1 = 2000;
const checkSingleElementCondition = async (locator, fnName, options) => {
  const startTime = getTimeStamp();
  const endTime = startTime + maxTimeout$1;
  let currentTime = startTime;
  const fn = SingleElementConditions[fnName];
  while (currentTime < endTime) {
    const element = querySelectorWithOptions(locator._selector, {
      hasText: locator._hasText,
      nth: locator._nth
    });
    if (element) {
      const successful = fn(element, options);
      if (successful) {
        return {
          error: false
        };
      }
    }
    await waitForMutation(100);
    currentTime = getTimeStamp();
  }
  return {
    error: true
  };
};

const createObjectUrl = blob => {
  const url = URL.createObjectURL(blob);
  return url;
};

class AssertionError extends Error {
  constructor(message) {
    super(message);
    this.name = 'AssertionError';
  }
}
const getType = value => {
  switch (typeof value) {
    case 'number':
      return 'number';
    case 'function':
      return 'function';
    case 'string':
      return 'string';
    case 'object':
      if (value === null) {
        return 'null';
      }
      if (Array.isArray(value)) {
        return 'array';
      }
      return 'object';
    case 'boolean':
      return 'boolean';
    default:
      return 'unknown';
  }
};
const object = value => {
  const type = getType(value);
  if (type !== 'object') {
    throw new AssertionError('expected value to be of type object');
  }
};
const string = value => {
  const type = getType(value);
  if (type !== 'string') {
    throw new AssertionError('expected value to be of type string');
  }
};

const Click = 'click';
const ContextMenu = 'contextmenu';
const KeyDown = 'keydown';
const KeyUp = 'keyup';
const MouseDown = 'mousedown';
const MouseEnter = 'mouseenter';
const MouseUp = 'mouseup';
const PointerDown = 'pointerdown';
const PointerMove = 'pointermove';
const PointerUp = 'pointerup';
const Wheel = 'wheel';

const mouseEvent = (element, eventType, options) => {
  const event = new MouseEvent(eventType, options);
  element.dispatchEvent(event);
};
const mouseDown = (element, options) => {
  mouseEvent(element, MouseDown, options);
};
const mouseUp = (element, options) => {
  mouseEvent(element, MouseUp, options);
};
const contextMenu = (element, options) => {
  mouseEvent(element, ContextMenu, options);
};
const click = (element, options) => {
  mouseDown(element, options);
  mouseEvent(element, Click, options);
  mouseUp(element, options);
  if (options.button === 2 /* right */) {
    contextMenu(element, options);
  }
};
const hover = (element, options) => {
  mouseEvent(element, MouseEnter, options);
};
const type = (element, options) => {
  element.value = options.text;
};
const keyboardEvent = (element, eventType, options) => {
  const event = new KeyboardEvent(eventType, options);
  element.dispatchEvent(event);
};
const keyDown = (element, options) => {
  keyboardEvent(element, KeyDown, options);
};
const keyUp = (element, options) => {
  keyboardEvent(element, KeyUp, options);
};
const getEventClass = eventType => {
  switch (eventType) {
    case Wheel:
      return WheelEvent;
    case PointerDown:
    case PointerUp:
    case PointerMove:
      return PointerEvent;
    default:
      return Event;
  }
};
const dispatchEvent = (element, options) => {
  const EventClass = getEventClass(options.type);
  const event = new EventClass(options.type, options.init);
  element.dispatchEvent(event);
};

var ElementActions = /*#__PURE__*/Object.freeze({
  __proto__: null,
  click: click,
  contextMenu: contextMenu,
  dispatchEvent: dispatchEvent,
  hover: hover,
  keyDown: keyDown,
  keyUp: keyUp,
  keyboardEvent: keyboardEvent,
  mouseDown: mouseDown,
  mouseEvent: mouseEvent,
  mouseUp: mouseUp,
  type: type
});

const maxTimeout = 2000;
const performAction = async (locator, fnName, options) => {
  object(locator);
  string(fnName);
  object(options);
  const startTime = getTimeStamp();
  const endTime = startTime + maxTimeout;
  let currentTime = startTime;
  const fn = ElementActions[fnName];
  while (currentTime < endTime) {
    const element = querySelectorWithOptions(locator._selector, {
      hasText: locator._hasText,
      nth: locator._nth
    });
    if (element) {
      fn(element, options);
      return;
    }
    await waitForMutation(100);
    currentTime = getTimeStamp();
  }
};

const testCommandMap = {
  createObjectUrl: createObjectUrl,
  'TestFrameWork.performAction': performAction,
  'TestFrameWork.checkConditionError': checkConditionError,
  'TestFrameWork.checkSingleElementCondition': checkSingleElementCondition
};

const handleMessageFromTestPort = async event => {
  const {
    data,
    target
  } = event;
  const {
    method,
    params,
    id
  } = data;
  const fn = testCommandMap[method];
  if (!fn) {
    throw new Error('unsupported method');
  }
  const result = await fn(...params);
  target.postMessage({
    jsonrpc: '2.0',
    id,
    result
  });
};

const isJsonRpcResponse = message => {
  return 'result' in message || 'error' in message;
};
const handleMessage = async event => {
  const message = event.data;
  if (isJsonRpcResponse(message)) {
    resolve(message.id, message);
    return;
  }
  const {
    method,
    params
  } = message;
  const result = await execute(method, ...params);
  if (message.id) {
    event.target.postMessage({
      jsonrpc: '2.0',
      id: message.id,
      result
    });
  }
};
const handleWindowMessage = event => {
  const {
    data
  } = event;
  const message = data;
  const innerPort = message.params[0];
  const portType = message.params[1];
  if (portType === 'test') {
    innerPort.onmessage = handleMessageFromTestPort;
    innerPort.postMessage('ready');
  } else {
    innerPort.onmessage = handleMessage;
    innerPort.postMessage('ready');
    set(innerPort);
  }
};

const main = () => {
  window.addEventListener('message', handleWindowMessage);
  const apiKey = 'lvceRpc';
  // @ts-ignore
  globalThis[apiKey] = apiFactory;
};

main();
