import { SerialAPIParser, ZWaveSerialFrameType, } from "@zwave-js/serial";
import { MockPort } from "@zwave-js/serial/mock";
import { noop } from "@zwave-js/shared";
import { createDeferredPromise } from "alcalzone-shared/deferred-promise";
import net from "node:net";
import { tmpdir } from "node:os";
import path from "pathe";
import { Driver } from "./Driver.js";
/** Exposes a {@link MockPort} via a local TCP server, so the driver can connect to it using a `tcp://` connection string */
async function serveMockPortViaTCP(mockPort) {
    // Take the role the driver would normally have in direct mode and
    // bridge between the mock port binding and the TCP socket
    const binding = await mockPort.factory()();
    let socket;
    // Forward data from the mock controller to the connected socket
    void new ReadableStream(binding.source).pipeTo(new WritableStream({
        write(chunk) {
            socket?.write(chunk);
        },
    })).catch(noop);
    const sinkWriter = new WritableStream(binding.sink).getWriter();
    const server = net.createServer((sock) => {
        socket = sock;
        // The mock controller expects one complete message per chunk, but TCP
        // does not preserve write boundaries. Re-chunk the byte stream into
        // Serial API frames before passing them on.
        const parser = new SerialAPIParser();
        const parserWriter = parser.writable.getWriter();
        void parser.readable.pipeTo(new WritableStream({
            write(frame) {
                if (frame.type !== ZWaveSerialFrameType.SerialAPI)
                    return;
                const data = typeof frame.data === "number"
                    ? Uint8Array.from([frame.data])
                    : frame.data;
                void sinkWriter.write(data).catch(noop);
            },
        })).catch(noop);
        sock.on("data", (chunk) => {
            void parserWriter.write(chunk).catch(noop);
        });
        sock.on("error", noop);
        sock.on("close", () => {
            if (socket === sock)
                socket = undefined;
            void parserWriter.abort().catch(noop);
        });
    });
    await new Promise((resolve) => {
        server.listen(0, "127.0.0.1", resolve);
    });
    return server;
}
/** Creates a real driver instance with a mocked serial port to enable end to end tests */
export async function createAndStartDriverWithMockPort(options = {}) {
    const { connectViaTCP = false, ...driverOptions } = options;
    const mockPort = new MockPort();
    let port;
    let tcpServer;
    if (connectViaTCP) {
        tcpServer = await serveMockPortViaTCP(mockPort);
        const address = tcpServer.address();
        port = `tcp://${address.address}:${address.port}`;
    }
    else {
        port = mockPort.factory();
    }
    return new Promise((resolve, reject) => {
        let driver;
        // This will be called when the driver has opened the serial port
        const onSerialPortOpen = (serial) => {
            // Return the info to the calling code, giving it control over
            // continuing the driver startup.
            const continuePromise = createDeferredPromise();
            resolve({
                driver,
                mockPort,
                serial,
                tcpServer,
                continueStartup: () => continuePromise.resolve(),
            });
            return continuePromise;
        };
        // Usually we don't want logs in these tests
        if (!driverOptions.logConfig) {
            driverOptions.logConfig = {
                enabled: false,
            };
        }
        const testingHooks = {
            ...driverOptions.testingHooks,
            onSerialPortOpen,
        };
        driver = new Driver(port, {
            ...driverOptions,
            testingHooks,
        });
        driver.start().catch(reject);
    });
}
export async function createAndStartTestingDriver(options = {}) {
    const { beforeStartup, skipControllerIdentification = false, skipFirmwareIdentification = true, skipNodeInterview = false, loadConfiguration = true, fs = (await import("#default_bindings/fs")).fs, ...internalOptions } = options;
    // Use a new fake serial port for each test
    const testId = Math.round(Math.random() * 0xffffffff)
        .toString(16)
        .padStart(8, "0");
    // internalOptions.portAddress ??= `/tty/FAKE${testId}`;
    if (skipControllerIdentification) {
        internalOptions.testingHooks ??= {};
        internalOptions.testingHooks.skipControllerIdentification = true;
    }
    if (skipNodeInterview) {
        internalOptions.testingHooks ??= {};
        internalOptions.testingHooks.skipNodeInterview = true;
    }
    if (!loadConfiguration) {
        internalOptions.testingHooks ??= {};
        internalOptions.testingHooks.loadConfiguration = false;
    }
    if (skipFirmwareIdentification) {
        internalOptions.testingHooks ??= {};
        internalOptions.testingHooks.skipFirmwareIdentification = true;
    }
    // TODO: Make sure we delete this from time to time
    const cacheDir = path.join(tmpdir(), "zwave-js-test-cache", testId);
    internalOptions.storage ??= {};
    internalOptions.storage.cacheDir = cacheDir;
    const { driver, continueStartup, mockPort, serial } = await createAndStartDriverWithMockPort(internalOptions);
    if (typeof beforeStartup === "function") {
        await beforeStartup(mockPort, serial);
    }
    // Make sure the mock FS gets restored when the driver is destroyed
    const originalDestroy = driver.destroy.bind(driver);
    driver.destroy = async () => {
        await originalDestroy();
        await fs.deleteDir(cacheDir);
    };
    return new Promise((resolve) => {
        driver.once("driver ready", () => {
            resolve({ driver, mockPort, serial });
        });
        continueStartup();
    });
}
//# sourceMappingURL=DriverMock.js.map