import { NOT_KNOWN, SecurityClass, SecurityManager, SecurityManager2, dskToString, generateECDHKeyPair, isCCInfoEqual, securityClassOrder, } from "@zwave-js/core";
import { TimedExpectation } from "@zwave-js/shared";
import { getDefaultMockEndpointCapabilities, getDefaultMockNodeCapabilities, } from "./MockNodeCapabilities.js";
import { MOCK_FRAME_ACK_TIMEOUT, MockZWaveFrameType, createMockZWaveAckFrame, createMockZWaveRequestFrame, } from "./MockZWaveFrame.js";
const defaultCCInfo = {
    isSupported: true,
    isControlled: false,
    secure: false,
    version: 1,
};
export class MockEndpoint {
    constructor(options) {
        this.index = options.index;
        this.node = options.node;
        const { commandClasses = [], ...capabilities } = options.capabilities
            ?? {};
        this.capabilities = {
            ...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 = 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 == undefined || !isCCInfoEqual(original, updated)) {
            this.implementedCCs.set(cc, updated);
        }
    }
    /** Removes information about a CC from this mock node */
    removeCC(cc) {
        this.implementedCCs.delete(cc);
    }
}
/** A mock node that can be used to test the driver as if it were speaking to an actual network */
export class 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;
        // Storage for remembering which security classes are granted to which nodes
        const securityClasses = new Map();
        // We internally use this to keep track of our own granted security classes. Set that up here
        const ownSecurityClasses = new Map();
        securityClasses.set(this.id, ownSecurityClasses);
        for (const secClass of options.capabilities?.securityClasses ?? []) {
            ownSecurityClasses.set(secClass, true);
        }
        // Set up capabilities and endpoints
        const { commandClasses = [], endpoints = [], ...capabilities } = options.capabilities ?? {};
        this.capabilities = {
            ...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) ?? NOT_KNOWN);
            },
            setSecurityClass(nodeId, securityClass, granted) {
                if (!securityClasses.has(nodeId)) {
                    securityClasses.set(nodeId, new Map());
                }
                securityClasses.get(nodeId).set(securityClass, granted);
            },
            getHighestSecurityClass(nodeId) {
                const map = securityClasses.get(nodeId);
                if (!map?.size)
                    return undefined;
                let missingSome = false;
                for (const secClass of securityClassOrder) {
                    if (map.get(secClass) === true)
                        return secClass;
                    if (!map.has(secClass)) {
                        missingSome = true;
                    }
                }
                // If we don't have the info for every security class, we don't know the highest one yet
                return missingSome ? undefined : SecurityClass.None;
            },
            getSupportedCCVersion: (cc, nodeId, endpointIndex = 0) => {
                // Mock endpoints only care about the version they implement
                const endpoint = this.endpoints.get(endpointIndex);
                return (endpoint ?? this).implementedCCs.get(cc)?.version ?? 0;
            },
            // Mock nodes don't care about device configuration files
            getDeviceConfig: () => undefined,
            get securityManager() {
                return self.securityManagers.securityManager;
            },
            get securityManager2() {
                return self.securityManagers.securityManager2;
            },
            get securityManagerLR() {
                return self.securityManagers.securityManagerLR;
            },
        };
    }
    _options;
    id;
    controller;
    capabilities;
    securityManagers = {
        securityManager: undefined,
        securityManager2: undefined,
        securityManagerLR: undefined,
    };
    // These will be set during security setup
    ecdhKeyPair;
    get dsk() {
        // The DSK is the first 16 bytes of the public key
        const dskBytes = this.ecdhKeyPair.publicKey.slice(1, 17);
        return dskToString(dskBytes);
    }
    get s2Pin() {
        // The S2 PIN is the first group of 5 digits in the DSK
        return this.dsk.slice(0, 5);
    }
    async setupSecurity() {
        this.ecdhKeyPair = await generateECDHKeyPair();
        const securityClasses = this._options.capabilities?.securityClasses;
        if (!securityClasses)
            return;
        // Set up security managers depending on the provided keys
        let securityManager;
        if (securityClasses.has(SecurityClass.S0_Legacy)
            && this._options.controller.securityManagers.securityManager) {
            securityManager = new SecurityManager({
                ownNodeId: this.id,
                networkKey: this._options.controller.securityManagers.securityManager
                    .networkKey,
                // Use a high nonce timeout to allow debugging tests more easily
                nonceTimeout: 100000,
            });
            // Remember that the controller has the S0 key
            this.encodingContext.setSecurityClass(this.controller.ownNodeId, SecurityClass.S0_Legacy, true);
        }
        let securityManager2 = undefined;
        if (this._options.controller.securityManagers.securityManager2
            && [
                SecurityClass.S2_AccessControl,
                SecurityClass.S2_Authenticated,
                SecurityClass.S2_Unauthenticated,
            ].some((secClass) => securityClasses.has(secClass))) {
            securityManager2 = await SecurityManager2.create();
            const controllerSm2 = this._options.controller.securityManagers.securityManager2;
            // Copy keys from the controller
            for (const secClass of [
                SecurityClass.S2_AccessControl,
                SecurityClass.S2_Authenticated,
                SecurityClass.S2_Unauthenticated,
            ]) {
                const key = controllerSm2.getKeysForSecurityClass(secClass)
                    ?.pnk;
                if (key) {
                    await securityManager2.setKey(secClass, key);
                    // Remember that the controller has this
                    this.encodingContext.setSecurityClass(this.controller.ownNodeId, secClass, true);
                }
            }
        }
        if (securityManager && securityManager2) {
            // Copy S0 key over
            await securityManager2.setKey(SecurityClass.S0_Legacy, securityManager.networkKey);
        }
        const securityManagerLR = undefined;
        this.securityManagers = {
            securityManager,
            securityManager2,
            securityManagerLR,
        };
    }
    encodingContext;
    behaviors = [];
    implementedCCs = new Map();
    endpoints = new Map();
    /** Can be used by behaviors to store controller related state */
    state = 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 = 5000, preventDefault = false, errorMessage = "The controller did not send the expected frame within the provided timeout!", } = options ?? {};
        const expectation = new 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 === MockZWaveFrameType.ACK, { timeout, errorMessage });
    }
    /**
     * Sends a {@link MockZWaveFrame} to the {@link MockController}
     */
    async sendToController(frame) {
        this.controller["air"].add({
            source: this.id,
            onTransmit: (frame) => {
                this.sentControllerFrames.push(frame);
            },
            ...frame,
        });
        if (frame.type === MockZWaveFrameType.Request && frame.ackRequested) {
            return await this.expectControllerACK(MOCK_FRAME_ACK_TIMEOUT);
        }
    }
    /** Gets called when a {@link MockZWaveFrame} is received from the {@link MockController} */
    async onControllerFrame(frame) {
        this.receivedControllerFrames.push(frame);
        // Ack the frame if desired
        if (this.autoAckControllerFrames
            && frame.type === MockZWaveFrameType.Request) {
            void this.ackControllerRequestFrame(frame);
        }
        // Handle message buffer. Check for pending expectations first.
        const handlers = this.expectedControllerFrames.filter((e) => !e.predicate || e.predicate(frame));
        // Resolve all matching expectations
        for (const handler of handlers) {
            handler.resolve(frame);
        }
        // If any handler has preventDefault set, skip default behaviors
        if (handlers.some((h) => h.preventDefault)) {
            return;
        }
        if (frame.type === MockZWaveFrameType.Request) {
            let cc = frame.payload;
            let response;
            // Transform incoming frames with hooks, e.g. to support unwrapping encapsulated CCs
            for (const behavior of this.behaviors) {
                if (behavior.transformIncomingCC) {
                    cc = await behavior.transformIncomingCC(this.controller, this, cc);
                }
            }
            // Figure out what to do with the frame
            for (const behavior of this.behaviors) {
                response = await behavior.handleCC?.(this.controller, this, cc);
                if (response)
                    break;
            }
            // If no behavior handled the frame, or we're supposed to stop, stop
            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(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) {
        // Iterate in reverse order so wrapping happens opposite to unwrapping.
        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(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 == undefined || !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) {
        // New behaviors must override existing ones, so we insert at the front of the array
        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;
        }
    }
}
//# sourceMappingURL=MockNode.js.map