"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 MockNode_exports = {};
__export(MockNode_exports, {
  MockEndpoint: () => MockEndpoint,
  MockNode: () => MockNode
});
module.exports = __toCommonJS(MockNode_exports);
var import_core = require("@zwave-js/core");
var import_shared = require("@zwave-js/shared");
var import_MockNodeCapabilities = require("./MockNodeCapabilities.js");
var import_MockZWaveFrame = require("./MockZWaveFrame.js");
const defaultCCInfo = {
  isSupported: true,
  isControlled: false,
  secure: false,
  version: 1
};
class MockEndpoint {
  static {
    __name(this, "MockEndpoint");
  }
  constructor(options) {
    this.index = options.index;
    this.node = options.node;
    const { commandClasses = [], ...capabilities } = options.capabilities ?? {};
    this.capabilities = {
      ...(0, import_MockNodeCapabilities.getDefaultMockEndpointCapabilities)(this.node.capabilities),
      ...capabilities
    };
    for (const cc of commandClasses) {
      if (typeof cc === "number") {
        this.addCC(cc, {});
      } else {
        const { ccId, ...ccInfo } = cc;
        this.addCC(ccId, ccInfo);
      }
    }
  }
  index;
  node;
  capabilities;
  implementedCCs = /* @__PURE__ */ new Map();
  /** Adds information about a CC to this mock endpoint */
  addCC(cc, info) {
    const original = this.implementedCCs.get(cc);
    const updated = Object.assign({}, original ?? defaultCCInfo, info);
    if (original == void 0 || !(0, import_core.isCCInfoEqual)(original, updated)) {
      this.implementedCCs.set(cc, updated);
    }
  }
  /** Removes information about a CC from this mock node */
  removeCC(cc) {
    this.implementedCCs.delete(cc);
  }
}
class MockNode {
  static {
    __name(this, "MockNode");
  }
  static async create(options) {
    const node = new MockNode(options);
    await node.setupSecurity();
    return node;
  }
  constructor(options) {
    this._options = options;
    this.id = options.id;
    this.controller = options.controller;
    const securityClasses = /* @__PURE__ */ new Map();
    const ownSecurityClasses = /* @__PURE__ */ new Map();
    securityClasses.set(this.id, ownSecurityClasses);
    for (const secClass of options.capabilities?.securityClasses ?? []) {
      ownSecurityClasses.set(secClass, true);
    }
    const { commandClasses = [], endpoints = [], ...capabilities } = options.capabilities ?? {};
    this.capabilities = {
      ...(0, import_MockNodeCapabilities.getDefaultMockNodeCapabilities)(),
      ...capabilities
    };
    for (const cc of commandClasses) {
      if (typeof cc === "number") {
        this.addCC(cc, {});
      } else {
        const { ccId, ...ccInfo } = cc;
        this.addCC(ccId, ccInfo);
      }
    }
    let index = 0;
    for (const endpoint of endpoints) {
      index++;
      this.endpoints.set(index, new MockEndpoint({
        index,
        node: this,
        capabilities: endpoint
      }));
    }
    const self = this;
    this.encodingContext = {
      homeId: this.controller.homeId,
      ownNodeId: this.id,
      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) => {
        const endpoint = this.endpoints.get(endpointIndex);
        return (endpoint ?? this).implementedCCs.get(cc)?.version ?? 0;
      }, "getSupportedCCVersion"),
      // Mock nodes don't care about device configuration files
      getDeviceConfig: /* @__PURE__ */ __name(() => void 0, "getDeviceConfig"),
      get securityManager() {
        return self.securityManagers.securityManager;
      },
      get securityManager2() {
        return self.securityManagers.securityManager2;
      },
      get securityManagerLR() {
        return self.securityManagers.securityManagerLR;
      }
    };
  }
  _options;
  id;
  controller;
  capabilities;
  securityManagers = {
    securityManager: void 0,
    securityManager2: void 0,
    securityManagerLR: void 0
  };
  // These will be set during security setup
  ecdhKeyPair;
  get dsk() {
    const dskBytes = this.ecdhKeyPair.publicKey.slice(1, 17);
    return (0, import_core.dskToString)(dskBytes);
  }
  get s2Pin() {
    return this.dsk.slice(0, 5);
  }
  async setupSecurity() {
    this.ecdhKeyPair = await (0, import_core.generateECDHKeyPair)();
    const securityClasses = this._options.capabilities?.securityClasses;
    if (!securityClasses)
      return;
    let securityManager;
    if (securityClasses.has(import_core.SecurityClass.S0_Legacy) && this._options.controller.securityManagers.securityManager) {
      securityManager = new import_core.SecurityManager({
        ownNodeId: this.id,
        networkKey: this._options.controller.securityManagers.securityManager.networkKey,
        // Use a high nonce timeout to allow debugging tests more easily
        nonceTimeout: 1e5
      });
      this.encodingContext.setSecurityClass(this.controller.ownNodeId, import_core.SecurityClass.S0_Legacy, true);
    }
    let securityManager2 = void 0;
    if (this._options.controller.securityManagers.securityManager2 && [
      import_core.SecurityClass.S2_AccessControl,
      import_core.SecurityClass.S2_Authenticated,
      import_core.SecurityClass.S2_Unauthenticated
    ].some((secClass) => securityClasses.has(secClass))) {
      securityManager2 = await import_core.SecurityManager2.create();
      const controllerSm2 = this._options.controller.securityManagers.securityManager2;
      for (const secClass of [
        import_core.SecurityClass.S2_AccessControl,
        import_core.SecurityClass.S2_Authenticated,
        import_core.SecurityClass.S2_Unauthenticated
      ]) {
        const key = controllerSm2.getKeysForSecurityClass(secClass)?.pnk;
        if (key) {
          await securityManager2.setKey(secClass, key);
          this.encodingContext.setSecurityClass(this.controller.ownNodeId, secClass, true);
        }
      }
    }
    if (securityManager && securityManager2) {
      await securityManager2.setKey(import_core.SecurityClass.S0_Legacy, securityManager.networkKey);
    }
    const securityManagerLR = void 0;
    this.securityManagers = {
      securityManager,
      securityManager2,
      securityManagerLR
    };
  }
  encodingContext;
  behaviors = [];
  implementedCCs = /* @__PURE__ */ new Map();
  endpoints = /* @__PURE__ */ new Map();
  /** Can be used by behaviors to store controller related state */
  state = /* @__PURE__ */ new Map();
  /** Controls whether the node automatically ACKs CCs from the controller before handling them */
  autoAckControllerFrames = true;
  expectedControllerFrames = [];
  /** Records the frames received from the controller to perform assertions on them */
  receivedControllerFrames = [];
  /** Records the frames sent to the controller to perform assertions on them */
  sentControllerFrames = [];
  /**
   * Waits until the controller sends a frame matching the given predicate or a timeout has elapsed.
   *
   * @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 expectControllerFrame(predicate, options) {
    const { timeout = 5e3, preventDefault = false, errorMessage = "The controller did not send the expected frame within the provided timeout!" } = options ?? {};
    const expectation = new import_shared.TimedExpectation(timeout, predicate, errorMessage, preventDefault);
    try {
      this.expectedControllerFrames.push(expectation);
      return await expectation;
    } finally {
      const index = this.expectedControllerFrames.indexOf(expectation);
      if (index !== -1) {
        void this.expectedControllerFrames.splice(index, 1);
      }
    }
  }
  /**
   * 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
   */
  expectControllerACK(timeout, errorMessage) {
    return this.expectControllerFrame((msg) => msg.type === import_MockZWaveFrame.MockZWaveFrameType.ACK, { timeout, errorMessage });
  }
  /**
   * Sends a {@link MockZWaveFrame} to the {@link MockController}
   */
  async sendToController(frame) {
    this.controller["air"].add({
      source: this.id,
      onTransmit: /* @__PURE__ */ __name((frame2) => {
        this.sentControllerFrames.push(frame2);
      }, "onTransmit"),
      ...frame
    });
    if (frame.type === import_MockZWaveFrame.MockZWaveFrameType.Request && frame.ackRequested) {
      return await this.expectControllerACK(import_MockZWaveFrame.MOCK_FRAME_ACK_TIMEOUT);
    }
  }
  /** Gets called when a {@link MockZWaveFrame} is received from the {@link MockController} */
  async onControllerFrame(frame) {
    this.receivedControllerFrames.push(frame);
    if (this.autoAckControllerFrames && frame.type === import_MockZWaveFrame.MockZWaveFrameType.Request) {
      void this.ackControllerRequestFrame(frame);
    }
    const handlers = this.expectedControllerFrames.filter((e) => !e.predicate || e.predicate(frame));
    for (const handler of handlers) {
      handler.resolve(frame);
    }
    if (handlers.some((h) => h.preventDefault)) {
      return;
    }
    if (frame.type === import_MockZWaveFrame.MockZWaveFrameType.Request) {
      let cc = frame.payload;
      let response;
      for (const behavior of this.behaviors) {
        if (behavior.transformIncomingCC) {
          cc = await behavior.transformIncomingCC(this.controller, this, cc);
        }
      }
      for (const behavior of this.behaviors) {
        response = await behavior.handleCC?.(this.controller, this, cc);
        if (response)
          break;
      }
      if (!response || response.action === "stop")
        return;
      await this.sendResponse(cc, response, frame);
    }
  }
  /**
   * Sends an ACK frame to the {@link MockController}
   */
  async ackControllerRequestFrame(frame) {
    await this.sendToController((0, import_MockZWaveFrame.createMockZWaveAckFrame)({
      repeaters: frame?.repeaters
    }));
  }
  /**
   * Applies all response transformation hooks to a mock response and sends the
   * final result to the controller.
   */
  async sendResponse(receivedCC, response, frame) {
    for (let i = this.behaviors.length - 1; i >= 0; i--) {
      const behavior = this.behaviors[i];
      if (behavior.transformResponse) {
        response = await behavior.transformResponse(this.controller, this, receivedCC, response);
      }
    }
    if (response.action === "sendCC") {
      await this.sendToController((0, import_MockZWaveFrame.createMockZWaveRequestFrame)(response.cc, {
        ackRequested: response.ackRequested
      }));
    } else if (response.action === "ack" && frame) {
      await this.ackControllerRequestFrame(frame);
    }
  }
  /** Adds information about a CC to this mock node */
  addCC(cc, info) {
    const original = this.implementedCCs.get(cc);
    const updated = Object.assign({}, original ?? defaultCCInfo, info);
    if (original == void 0 || !(0, import_core.isCCInfoEqual)(original, updated)) {
      this.implementedCCs.set(cc, updated);
    }
  }
  /** Removes information about a CC from this mock node */
  removeCC(cc) {
    this.implementedCCs.delete(cc);
  }
  defineBehavior(...behaviors) {
    this.behaviors.unshift(...behaviors);
  }
  /** Asserts that a frame matching the given predicate was received from the controller */
  assertReceivedControllerFrame(predicate, options) {
    const { errorMessage, noMatch } = options ?? {};
    const index = this.receivedControllerFrames.findIndex(predicate);
    if (index === -1 && !noMatch) {
      throw new Error(`Node ${this.id} did not receive a Z-Wave frame matching the predicate!${errorMessage ? ` ${errorMessage}` : ""}`);
    } else if (index > -1 && noMatch) {
      throw new Error(`Node ${this.id} received a Z-Wave frame matching the predicate, but this was not expected!${errorMessage ? ` ${errorMessage}` : ""}`);
    }
  }
  /** Forgets all recorded frames received from the controller */
  clearReceivedControllerFrames() {
    this.receivedControllerFrames = [];
  }
  /** Asserts that a frame matching the given predicate was sent to the controller */
  assertSentControllerFrame(predicate, options) {
    const { errorMessage, noMatch } = options ?? {};
    const index = this.sentControllerFrames.findIndex(predicate);
    if (index === -1 && !noMatch) {
      throw new Error(`Node ${this.id} did not send a Z-Wave frame matching the predicate!${errorMessage ? ` ${errorMessage}` : ""}`);
    } else if (index > -1 && noMatch) {
      throw new Error(`Node ${this.id} sent a Z-Wave frame matching the predicate, but this was not expected!${errorMessage ? ` ${errorMessage}` : ""}`);
    }
  }
  /** Forgets all recorded frames sent to the controller */
  clearSentControllerFrames() {
    this.sentControllerFrames = [];
  }
  getCCCapabilities(ccId, endpointIndex) {
    let ccInfo;
    if (endpointIndex) {
      const endpoint = this.endpoints.get(endpointIndex);
      ccInfo = endpoint?.implementedCCs.get(ccId);
    } else {
      ccInfo = this.implementedCCs.get(ccId);
    }
    if (ccInfo) {
      const { isSupported, isControlled, version, secure, ...ret } = ccInfo;
      return ret;
    }
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  MockEndpoint,
  MockNode
});
//# sourceMappingURL=MockNode.js.map
