"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var MockController_exports = {};
__export(MockController_exports, {
  MockController: () => MockController
});
module.exports = __toCommonJS(MockController_exports);
var import_core = require("@zwave-js/core");
var import_serial = require("@zwave-js/serial");
var import_shared = require("@zwave-js/shared");
var import_async = require("alcalzone-shared/async");
var import_MockControllerCapabilities = require("./MockControllerCapabilities.js");
var import_MockZWaveFrame = require("./MockZWaveFrame.js");
class MockController {
  static {
    __name(this, "MockController");
  }
  static async create(options) {
    const ret = new MockController(options);
    await ret.setupSecurityManagers();
    return ret;
  }
  constructor(options) {
    this._options = options;
    this.mockPort = options.mockPort;
    this.serial = options.serial;
    void this.handleSerialData();
    this.ownNodeId = options.ownNodeId ?? 1;
    this.homeId = options.homeId ?? 2119634944;
    this.capabilities = {
      ...(0, import_MockControllerCapabilities.getDefaultMockControllerCapabilities)(),
      ...options.capabilities
    };
    const securityClasses = /* @__PURE__ */ new Map();
    const requestStorage = /* @__PURE__ */ new Map();
    const self = this;
    this.encodingContext = {
      homeId: this.homeId,
      ownNodeId: this.ownNodeId,
      // TODO: LR is not supported in mocks
      nodeIdType: import_core.NodeIDType.Short,
      hasSecurityClass(nodeId, securityClass) {
        return securityClasses.get(nodeId)?.get(securityClass) ?? import_core.NOT_KNOWN;
      },
      setSecurityClass(nodeId, securityClass, granted) {
        if (!securityClasses.has(nodeId)) {
          securityClasses.set(nodeId, /* @__PURE__ */ new Map());
        }
        securityClasses.get(nodeId).set(securityClass, granted);
      },
      getHighestSecurityClass(nodeId) {
        const map = securityClasses.get(nodeId);
        if (!map?.size)
          return void 0;
        let missingSome = false;
        for (const secClass of import_core.securityClassOrder) {
          if (map.get(secClass) === true)
            return secClass;
          if (!map.has(secClass)) {
            missingSome = true;
          }
        }
        return missingSome ? void 0 : import_core.SecurityClass.None;
      },
      getSupportedCCVersion: /* @__PURE__ */ __name((cc, nodeId, endpointIndex = 0) => {
        if (!this.nodes.has(nodeId)) {
          return 0;
        }
        const node = this.nodes.get(nodeId);
        const endpoint = node.endpoints.get(endpointIndex);
        return (endpoint ?? node).implementedCCs.get(cc)?.version ?? 0;
      }, "getSupportedCCVersion"),
      getDeviceConfig: /* @__PURE__ */ __name(() => void 0, "getDeviceConfig"),
      get securityManager() {
        return self.securityManagers.securityManager;
      },
      get securityManager2() {
        return self.securityManagers.securityManager2;
      },
      get securityManagerLR() {
        return self.securityManagers.securityManagerLR;
      }
    };
    this.parsingContext = {
      ...this.encodingContext,
      // FIXME: Take from the controller capabilities
      sdkVersion: void 0,
      requestStorage
    };
    void this.execute().catch((e) => {
      console.error(e);
    });
  }
  _options;
  homeId;
  ownNodeId;
  securityManagers = {
    securityManager: void 0,
    securityManager2: void 0,
    securityManagerLR: void 0
  };
  async setupSecurityManagers() {
    let securityManager;
    if (this._options.securityKeys?.S0_Legacy) {
      securityManager = new import_core.SecurityManager({
        ownNodeId: this.ownNodeId,
        networkKey: this._options.securityKeys.S0_Legacy,
        // Use a high nonce timeout to allow debugging tests more easily
        nonceTimeout: 1e5
      });
    }
    let securityManager2 = void 0;
    if (this._options.securityKeys?.S2_AccessControl || this._options.securityKeys?.S2_Authenticated || this._options.securityKeys?.S2_Unauthenticated) {
      securityManager2 = await import_core.SecurityManager2.create();
      if (this._options.securityKeys.S2_AccessControl) {
        await securityManager2.setKey(import_core.SecurityClass.S2_AccessControl, this._options.securityKeys.S2_AccessControl);
      }
      if (this._options.securityKeys.S2_Authenticated) {
        await securityManager2.setKey(import_core.SecurityClass.S2_Authenticated, this._options.securityKeys.S2_Authenticated);
      }
      if (this._options.securityKeys.S2_Unauthenticated) {
        await securityManager2.setKey(import_core.SecurityClass.S2_Unauthenticated, this._options.securityKeys.S2_Unauthenticated);
      }
      if (this._options.securityKeys.S0_Legacy) {
        await securityManager2.setKey(import_core.SecurityClass.S0_Legacy, this._options.securityKeys.S0_Legacy);
      }
    }
    const securityManagerLR = void 0;
    this.securityManagers = {
      securityManager,
      securityManager2,
      securityManagerLR
    };
  }
  encodingContext;
  parsingContext;
  mockPort;
  serial;
  expectedHostACKs = [];
  expectedHostMessages = [];
  expectedNodeFrames = /* @__PURE__ */ new Map();
  behaviors = [];
  /** Shared medium for sending messages back and forth */
  air = new import_shared.AsyncQueue();
  /** Records the messages received from the host to perform assertions on them */
  _receivedHostMessages = [];
  get receivedHostMessages() {
    return this._receivedHostMessages;
  }
  _nodes = /* @__PURE__ */ new Map();
  get nodes() {
    return this._nodes;
  }
  addNode(node) {
    this._nodes.set(node.id, node);
  }
  removeNode(node) {
    this._nodes.delete(node.id);
  }
  capabilities;
  /** Can be used by behaviors to store controller related state */
  state = /* @__PURE__ */ new Map();
  /** Node info for the node that is pending inclusion. Set this before starting inclusion to simulate a node joining. */
  nodePendingInclusion;
  /** Controls whether the controller automatically ACKs messages from the host before handling them */
  autoAckHostMessages = true;
  /** Controls whether the controller automatically ACKs node frames before handling them */
  autoAckNodeFrames = true;
  /** Allows reproducing issues with the 7.19.x firmware where the high nibble of the ACK after soft-reset is corrupted */
  corruptACK = false;
  async handleSerialData() {
    try {
      read: for await (const data of this.mockPort.readable) {
        for (const behavior of this.behaviors) {
          if (await behavior.onHostData?.(this, data)) {
            continue read;
          }
        }
        if (data.length === 1) {
          const header = data[0];
          switch (header) {
            case import_serial.MessageHeaders.ACK:
            case import_serial.MessageHeaders.NAK:
            case import_serial.MessageHeaders.CAN:
              void this.serialOnData(header);
              continue;
          }
        }
        void this.serialOnData(data);
      }
    } catch (e) {
      if ((0, import_shared.isAbortError)(e))
        return;
      throw e;
    }
  }
  /** Gets called when parsed/chunked data is received from the serial port */
  async serialOnData(data) {
    if (typeof data === "number") {
      switch (data) {
        case import_serial.MessageHeaders.ACK: {
          this.expectedHostACKs?.shift()?.resolve();
          return;
        }
        case import_serial.MessageHeaders.NAK: {
          return;
        }
        case import_serial.MessageHeaders.CAN: {
          throw new Error("Mock controller received a CAN from the host. This is illegal!");
        }
      }
    }
    let msg;
    try {
      msg = import_serial.Message.parse(data, {
        ...this.parsingContext,
        origin: import_serial.MessageOrigin.Host
      });
      this._receivedHostMessages.push(msg);
      if (this.autoAckHostMessages) {
        this.ackHostMessage();
      }
    } catch (e) {
      debugger;
      throw new Error(`Mock controller received an invalid message from the host: ${e.stack}`);
    }
    const handlers = this.expectedHostMessages.filter((e) => !e.predicate || e.predicate(msg));
    for (const handler of handlers) {
      handler.resolve(msg);
    }
    if (handlers.some((h) => h.preventDefault)) {
      return;
    }
    for (const behavior of this.behaviors) {
      if (await behavior.onHostMessage?.(this, msg)) {
        return;
      }
    }
  }
  /**
   * Waits until the host sends an ACK or a timeout has elapsed.
   *
   * @param timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected
   */
  async expectHostACK(timeout, errorMessage) {
    const ack = new import_shared.TimedExpectation(timeout, void 0, errorMessage ?? "Host did not respond with an ACK within the provided timeout!");
    try {
      this.expectedHostACKs.push(ack);
      return await ack;
    } finally {
      const index = this.expectedHostACKs.indexOf(ack);
      if (index !== -1)
        void this.expectedHostACKs.splice(index, 1);
    }
  }
  /**
   * Waits until the host sends a message matching the given predicate or a timeout has elapsed.
   *
   * @param predicate A predicate function to test incoming messages
   * @param options Optional configuration
   * @param options.timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected. Default: 5000ms
   * @param options.preventDefault If true, the default behavior will not be executed after the expectation is fulfilled. Default: false
   */
  async expectHostMessage(predicate, options) {
    const { timeout = 5e3, preventDefault = false, errorMessage = "Host did not send the expected message within the provided timeout!" } = options ?? {};
    const expectation = new import_shared.TimedExpectation(timeout, predicate, errorMessage, preventDefault);
    try {
      this.expectedHostMessages.push(expectation);
      return await expectation;
    } finally {
      const index = this.expectedHostMessages.indexOf(expectation);
      if (index !== -1)
        void this.expectedHostMessages.splice(index, 1);
    }
  }
  /**
   * Waits until the node sends a message matching the given predicate or a timeout has elapsed.
   *
   * @param node The node to expect a frame from
   * @param predicate A predicate function to test incoming frames
   * @param options Optional configuration
   * @param options.timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected. Default: 5000ms
   * @param options.preventDefault If true, the default behavior will not be executed after the expectation is fulfilled. Default: false
   */
  async expectNodeFrame(node, predicate, options) {
    const { timeout = 5e3, preventDefault = false, errorMessage = `Node ${node.id} did not send the expected frame within the provided timeout!` } = options ?? {};
    const expectation = new import_shared.TimedExpectation(timeout, predicate, errorMessage, preventDefault);
    try {
      if (!this.expectedNodeFrames.has(node.id)) {
        this.expectedNodeFrames.set(node.id, []);
      }
      this.expectedNodeFrames.get(node.id).push(expectation);
      return await expectation;
    } finally {
      const array = this.expectedNodeFrames.get(node.id);
      if (array) {
        const index = array.indexOf(expectation);
        if (index !== -1)
          void array.splice(index, 1);
      }
    }
  }
  /**
   * Waits until the node sends a message matching the given predicate or a timeout has elapsed.
   *
   * @param node The node to expect a CC from
   * @param predicate A predicate function to test incoming CCs
   * @param options Optional configuration
   * @param options.timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected. Default: 5000ms
   * @param options.preventDefault If true, the default behavior will not be executed after the expectation is fulfilled. Default: false
   */
  async expectNodeCC(node, predicate, options) {
    const ret = await this.expectNodeFrame(node, (msg) => msg.type === import_MockZWaveFrame.MockZWaveFrameType.Request && predicate(msg.payload), options);
    return ret.payload;
  }
  /**
   * Waits until the controller sends an ACK frame or a timeout has elapsed.
   *
   * @param timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected
   */
  expectNodeACK(node, timeout, errorMessage) {
    return this.expectNodeFrame(node, (msg) => msg.type === import_MockZWaveFrame.MockZWaveFrameType.ACK, { timeout, errorMessage });
  }
  /** Sends a message header (ACK/NAK/CAN) to the host/driver */
  sendHeaderToHost(data) {
    this.mockPort.emitData(Uint8Array.from([data]));
  }
  /** Sends a raw buffer to the host/driver and expect an ACK */
  async sendMessageToHost(msg, fromNode) {
    let data;
    if (fromNode) {
      data = await msg.serialize({
        nodeIdType: this.encodingContext.nodeIdType,
        ...fromNode.encodingContext
      });
      await (0, import_async.wait)(fromNode.capabilities.txDelay);
    } else {
      data = await msg.serialize(this.encodingContext);
    }
    this.mockPort.emitData(data);
    await this.expectHostACK(1e3);
  }
  /** Sends a raw buffer to the host/driver and expect an ACK */
  async sendToHost(data) {
    this.mockPort.emitData(data);
    await this.expectHostACK(1e3);
  }
  /**
   * Sends an ACK frame to the host
   */
  ackHostMessage() {
    if (this.corruptACK) {
      const highNibble = (0, import_core.randomBytes)(1)[0] & 240;
      this.mockPort.emitData(Uint8Array.from([highNibble | import_serial.MessageHeaders.ACK]));
    } else {
      this.sendHeaderToHost(import_serial.MessageHeaders.ACK);
    }
  }
  /** Gets called when a {@link MockZWaveFrame} is received from a {@link MockNode} */
  async onNodeFrame(node, frame) {
    if (this.autoAckNodeFrames && frame.type === import_MockZWaveFrame.MockZWaveFrameType.Request) {
      void this.ackNodeRequestFrame(node, frame);
    }
    const handlers = this.expectedNodeFrames.get(node.id)?.filter((e) => !e.predicate || e.predicate(frame)) ?? [];
    for (const handler of handlers) {
      handler.resolve(frame);
    }
    if (handlers.some((h) => h.preventDefault)) {
      return;
    }
    for (const behavior of this.behaviors) {
      if (await behavior.onNodeFrame?.(this, node, frame)) {
        return;
      }
    }
  }
  /**
   * Sends an ACK frame to a {@link MockNode}
   */
  async ackNodeRequestFrame(node, frame) {
    await this.sendToNode(node, (0, import_MockZWaveFrame.createMockZWaveAckFrame)({
      repeaters: frame?.repeaters
    }));
  }
  /**
   * Sends a {@link MockZWaveFrame} to a {@link MockNode}
   */
  async sendToNode(node, frame) {
    this.air.add({
      target: node.id,
      ...frame
    });
    if (frame.type === import_MockZWaveFrame.MockZWaveFrameType.Request && frame.ackRequested) {
      return await this.expectNodeACK(node, import_MockZWaveFrame.MOCK_FRAME_ACK_TIMEOUT);
    }
  }
  defineBehavior(...behaviors) {
    this.behaviors.unshift(...behaviors);
  }
  /** Asserts that a message matching the given predicate was received from the host */
  assertReceivedHostMessage(predicate, options) {
    const { errorMessage } = options ?? {};
    const index = this._receivedHostMessages.findIndex(predicate);
    if (index === -1) {
      throw new Error(`Did not receive a host message matching the predicate!${errorMessage ? ` ${errorMessage}` : ""}`);
    }
  }
  /** Forgets all recorded messages received from the host */
  clearReceivedHostMessages() {
    this._receivedHostMessages = [];
  }
  async execute() {
    for await (const { source, target, onTransmit, ...frame } of this.air) {
      if (!source && target) {
        const node = this._nodes.get(target);
        if (!node)
          continue;
        await (0, import_async.wait)(node.capabilities.txDelay);
        const unlazy = await (0, import_MockZWaveFrame.unlazyMockZWaveFrame)(frame);
        onTransmit?.(unlazy);
        node.onControllerFrame(unlazy).catch((e) => {
          console.error(e);
        });
      } else if (source && !target) {
        const node = this._nodes.get(source);
        if (!node)
          continue;
        await (0, import_async.wait)(node.capabilities.txDelay);
        const unlazy = await (0, import_MockZWaveFrame.unlazyMockZWaveFrame)(frame);
        onTransmit?.(unlazy);
        this.onNodeFrame(node, unlazy).catch((e) => {
          console.error(e);
        });
      }
    }
  }
  destroy() {
    this.air.abort();
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  MockController
});
//# sourceMappingURL=MockController.js.map
