window.WebSocket = class MockWebSocket extends EventTarget {
  static CONNECTING = 0;
  static OPEN = 1;
  static CLOSING = 2;
  static CLOSED = 3;

  static #mocks = new Map();

  static getAll() {
    return this.#mocks.values();
  }

  static getByURL(url) {
    if (this.#mocks.has(url)) {
      return this.#mocks.get(url);
    }
    for (const [wsURL, ws] of this.#mocks) {
      if (wsURL.includes(url)) {
        return ws;
      }
    }
    return undefined;
  }

  #url;
  #protocols;
  #protocol = "";
  #binaryType = "blob";
  #bufferedAmount = 0;
  #extensions = "";
  #readyState = MockWebSocket.CONNECTING;

  #onopen = null;
  #onerror = null;
  #onmessage = null;
  #onclose = null;

  #spyMessage = null;
  #spyClose = null;

  constructor(url, protocols) {
    super();

    this.#url = url;
    this.#protocols = protocols || [];

    MockWebSocket.#mocks.set(this.#url, this);

    if (typeof window["onMockWebSocketConstructor"] === "function") {
      onMockWebSocketConstructor(this.#url, this.#protocols);
    }
    if (typeof window["onMockWebSocketSpyMessage"] === "function") {
      this.#spyMessage = onMockWebSocketSpyMessage;
    }
    if (typeof window["onMockWebSocketSpyClose"] === "function") {
      this.#spyClose = onMockWebSocketSpyClose;
    }
  }

  set binaryType(binaryType) {
    if (!["blob", "arraybuffer"].includes(binaryType)) {
      return;
    }
    this.#binaryType = binaryType;
  }

  get binaryType() {
    return this.#binaryType;
  }

  get bufferedAmount() {
    return this.#bufferedAmount;
  }

  get extensions() {
    return this.#extensions;
  }

  get readyState() {
    return this.#readyState;
  }

  get protocol() {
    return this.#protocol;
  }

  get url() {
    return this.#url;
  }

  set onopen(callback) {
    this.removeEventListener("open", this.#onopen);
    this.#onopen = null;

    if (typeof callback === "function") {
      this.addEventListener("open", callback);
      this.#onopen = callback;
    }
  }

  get onopen() {
    return this.#onopen;
  }

  set onerror(callback) {
    this.removeEventListener("error", this.#onerror);
    this.#onerror = null;

    if (typeof callback === "function") {
      this.addEventListener("error", callback);
      this.#onerror = callback;
    }
  }

  get onerror() {
    return this.#onerror;
  }

  set onmessage(callback) {
    this.removeEventListener("message", this.#onmessage);
    this.#onmessage = null;

    if (typeof callback === "function") {
      this.addEventListener("message", callback);
      this.#onmessage = callback;
    }
  }

  get onmessage() {
    return this.#onmessage;
  }

  set onclose(callback) {
    this.removeEventListener("close", this.#onclose);
    this.#onclose = null;

    if (typeof callback === "function") {
      this.addEventListener("close", callback);
      this.#onclose = callback;
    }
  }

  get onclose() {
    return this.#onclose;
  }

  get mockProtocols() {
    return this.#protocols;
  }

  spyClose(callback) {
    if (typeof callback !== "function") {
      throw new TypeError("Invalid callback");
    }
    this.#spyClose = callback;
    return this;
  }

  spyMessage(callback) {
    if (typeof callback !== "function") {
      throw new TypeError("Invalid callback");
    }
    this.#spyMessage = callback;
    return this;
  }

  mockOpen(options) {
    this.#protocol = options?.protocol || "";
    this.#extensions = options?.extensions || "";
    this.#readyState = MockWebSocket.OPEN;
    this.dispatchEvent(new Event("open"));
    return this;
  }

  mockError(error) {
    this.#readyState = MockWebSocket.CLOSED;
    this.dispatchEvent(new ErrorEvent("error", { error }));
    return this;
  }

  mockMessage(data) {
    if (this.#readyState !== MockWebSocket.OPEN) {
      throw new Error("MockWebSocket is not connected");
    }
    this.dispatchEvent(new MessageEvent("message", { data }));
    return this;
  }

  mockClose(code, reason) {
    this.#readyState = MockWebSocket.CLOSED;
    this.dispatchEvent(new CloseEvent("close", { code: code || 1000, reason: reason || "" }));
    return this;
  }

  send(data) {
    if (this.#readyState === MockWebSocket.CONNECTING) {
      throw new DOMException("InvalidStateError", "MockWebSocket is not connected");
    }

    if (this.#spyMessage) {
      this.#spyMessage(this.url, data);
    }
  }

  close(code, reason) {
    if (code && !Number.isInteger(code) && code !== 1000 && (code < 3000 || code > 4999)) {
      throw new DOMException("InvalidAccessError", "Invalid code");
    }

    if (reason && typeof reason === "string") {
      const reasonBytes = new TextEncoder().encode(reason);
      if (reasonBytes.length > 123) {
        throw new DOMException("SyntaxError", "Reason is too long");
      }
    }

    if ([MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState)) {
      return;
    }

    this.#readyState = MockWebSocket.CLOSING;
    if (this.#spyClose) {
      this.#spyClose(this.url, code, reason);
    }
  }
};
