"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.mockWebSocketProvider = exports.TestAdapter = exports.RedisMock = exports.MockCache = exports.NopTransport = exports.MockWebsocketServer = exports.MockWebsocket = void 0;
exports.runPeriodicAsyncBackgroundExecution = runPeriodicAsyncBackgroundExecution;
exports.runAllUntil = runAllUntil;
exports.runAllUntilTime = runAllUntilTime;
exports.assertEqualResponses = assertEqualResponses;
exports.waitUntilResolved = waitUntilResolved;
exports.setEnvVariables = setEnvVariables;
const mock_socket_1 = require("mock-socket");
const client = __importStar(require("prom-client"));
const cache_1 = require("../cache");
const index_1 = require("../index");
const index_2 = require("./index");
var mock_socket_2 = require("mock-socket");
Object.defineProperty(exports, "MockWebsocket", { enumerable: true, get: function () { return mock_socket_2.WebSocket; } });
Object.defineProperty(exports, "MockWebsocketServer", { enumerable: true, get: function () { return mock_socket_2.Server; } });
/* c8 ignore start */ // eslint-disable-line
class NopTransport {
    async initialize(dependencies, adapterSettings, endpointName, transportName) {
        this.responseCache = dependencies.responseCache;
        this.name = transportName;
        return;
    }
    async foregroundExecute(_) {
        return;
    }
}
exports.NopTransport = NopTransport;
class MockCache extends cache_1.LocalCache {
    constructor(maxItems) {
        super(maxItems);
    }
    waitForNextSet() {
        // eslint-disable-next-line no-promise-executor-return
        return new Promise((resolve) => (this.awaitingPromiseResolve = resolve));
    }
    async set(key, value, ttl) {
        super.set(key, value, ttl);
        if (this.awaitingPromiseResolve) {
            this.awaitingPromiseResolve(value);
        }
    }
}
exports.MockCache = MockCache;
async function runPeriodicAsyncBackgroundExecution(clock, { interval, times, stepValidation, }) {
    for (let i = 0; i < times; i++) {
        // Tick once for the interval
        await clock.tickAsync(interval);
        // Then use auxiliary method to ensure that the background process, if it
        // spawns new timers, gets executed to completion
        await runAllUntil(clock, () => stepValidation(i));
    }
}
async function runAllUntil(clock, isComplete) {
    while (!isComplete()) {
        await clock.nextAsync();
    }
}
async function runAllUntilTime(clock, time) {
    const targetTime = clock.now + time;
    // Fire a promise that will resolve at the requested time, so we have a precise place where we'll stop
    (0, index_2.sleep)(time);
    while (clock.now < targetTime) {
        await clock.nextAsync();
    }
}
class RedisMock {
    constructor() {
        this.store = new cache_1.LocalCache(10000);
        this.keys = {};
    }
    get(key) {
        return this.store.get(key);
    }
    del(key) {
        return this.store.delete(key);
    }
    set(key, value, px, ttl) {
        if (key.includes('force-error')) {
            throw { message: 'anything' };
        }
        return this.store.set(key, value, ttl);
    }
    multi() {
        return new CommandChainMock(this);
    }
    defineCommand(_name, _options) {
        return;
    }
    setExternalAdapterResponse(key, value, ttl) {
        return this.set(key, value, 'PX', ttl);
    }
    evalsha(...args) {
        const [redlockKey, instanceKey] = args[2];
        // If key has been used to acquire a lock, and the instance key matches, extend it
        if (this.keys[redlockKey] === instanceKey) {
            return 1;
        }
        // If key has not been used to acquire a lock, set it
        if (this.keys[redlockKey] === undefined) {
            this.keys[redlockKey] = instanceKey;
            return 1;
        }
        else {
            // If key has already been used, reject lock acquisition
            return 0;
        }
    }
    pexpire() {
        return;
    }
    // For cache lock tests - naive implementation as the necessary error will
    // already have been thrown and the test will end the process
    quit() {
        return;
    }
}
exports.RedisMock = RedisMock;
class CommandChainMock {
    constructor(redisMock) {
        this.redisMock = redisMock;
        this.promises = [];
    }
    set(key, value, px, ttl) {
        this.promises.push(this.redisMock.set(key, value, px, ttl));
        return this;
    }
    setExternalAdapterResponse(key, value, ttl) {
        this.promises.push(this.redisMock.setExternalAdapterResponse(key, value, ttl));
        return this;
    }
    exec() {
        return Promise.all(this.promises);
    }
}
function assertEqualResponses(t, actual, expected) {
    t.is(typeof actual?.timestamps?.providerDataReceivedUnixMs, 'number');
    t.is(typeof (actual?.timestamps?.providerDataReceivedUnixMs ??
        actual?.timestamps?.providerDataStreamEstablishedUnixMs), 'number');
    delete actual?.meta;
    delete actual['timestamps'];
    t.deepEqual(expected, actual);
}
class TestMetrics {
    replaceQuotes(s) {
        return s.replace(/\\"/g, "'");
    }
    constructor(data) {
        this.map = new Map();
        const lines = data.split('\n');
        for (const line of lines) {
            if (line.startsWith('#') ||
                line.startsWith('nodejs_') ||
                line.startsWith('process_') ||
                !line) {
                continue;
            }
            const [nameAndLabels, stringValue] = line.split(' ');
            const [, name, rawLabels] = nameAndLabels.match(/^([a-z_]+){(.*)}$/);
            const sortedLabels = this.replaceQuotes(rawLabels)
                .split('",')
                .filter((label) => label !== '' && !label.startsWith('app_'))
                .sort((a, b) => a.localeCompare(b))
                .map((s) => `${s}"`)
                .join(',');
            const fullName = `${name}|${sortedLabels}`;
            this.map.set(fullName, Number(stringValue));
        }
    }
    get(t, { name, labels }) {
        const sortedLabels = labels
            ? Object.entries(labels)
                .map(([labelName, value]) => `${labelName}="${this.replaceQuotes(value)}"`)
                .sort((a, b) => a.localeCompare(b))
                .join(',')
            : '';
        const metric = this.map.get(`${name}|${sortedLabels}`);
        if (metric === undefined) {
            const sameNameMetrics = [...this.map.keys()]
                .filter((k) => k.startsWith(name))
                .map((m) => `\n\t${m}`);
            const possibleSolutionMessage = sameNameMetrics.length
                ? `Perhaps you meant one of these: ${sameNameMetrics}`
                : 'Check the metric name and labels (no other metrics with the same name were found)';
            t.fail(`Metric not found:\n\t${name}|${sortedLabels}\n${possibleSolutionMessage}`);
        }
        return metric;
    }
    assert(t, params) {
        const metric = this.get(t, params);
        t.is(metric, params.expectedValue);
    }
    assertPositiveNumber(t, params) {
        const value = this.get(t, params);
        if (value !== undefined) {
            t.is(typeof value === 'number', true);
            t.is(value > 0, true);
        }
        else {
            t.fail(`${params.name} did not record`);
        }
    }
}
class TestAdapter {
    // eslint-disable-next-line max-params
    constructor(api, adapter, metricsApi, clock, cache) {
        this.api = api;
        this.adapter = adapter;
        this.metricsApi = metricsApi;
        this.clock = clock;
        if (cache instanceof MockCache) {
            this.mockCache = cache;
        }
    }
    static async startWithMockedCache(adapter, context, dependencies) {
        // Create mocked cache so we can listen when values are set
        // This is a more reliable method than expecting precise clock timings
        const mockCache = new MockCache(adapter.config.settings.CACHE_MAX_ITEMS);
        return TestAdapter.start(adapter, context, {
            cache: mockCache,
            ...dependencies,
        });
    }
    static async start(adapter, context, dependencies) {
        const { api, metricsApi } = await (0, index_1.start)(adapter, dependencies);
        if (!api) {
            throw new Error('EA was not able to start properly');
        }
        context.testAdapter = new TestAdapter(api, adapter, metricsApi, context.clock, dependencies?.cache);
        return context.testAdapter;
    }
    async request(data, headers) {
        const makeRequest = async () => this.api.inject({
            method: 'post',
            url: '/',
            headers: {
                'content-type': 'application/json',
                ...headers,
            },
            payload: {
                data,
            },
        });
        // If there's no installed clock, just return the normal response promise
        if (!this.clock) {
            return makeRequest();
        }
        return waitUntilResolved(this.clock, makeRequest);
    }
    async startBackgroundExecuteThenGetResponse(t, params) {
        if (!this.clock) {
            throw new Error('The "startBackgroundExecuteThenGetResponse" method should only be called if a fake clock is installed');
        }
        if (!this.mockCache) {
            throw new Error('The "startBackgroundExecuteThenGetResponse" method should only be called if a mock cache was provided');
        }
        // Expect the first response to time out
        // The polling behavior is tested in the cache tests, so this is easier here.
        // Start the request:
        const error = await this.request(params.requestData);
        t.is(error.statusCode, 504);
        await this.waitForCache(params.expectedCacheSize);
        // Second request should find the response in the cache
        const response = await this.request(params.requestData);
        if (params.expectedResponse) {
            assertEqualResponses(t, response.json(), params.expectedResponse);
        }
        else {
            t.is(response.statusCode, 200);
        }
        return response;
    }
    async waitForCache(expectedSize) {
        if (!this.clock) {
            throw new Error('The "startBackgroundExecuteThenGetResponse" method should only be called if a fake clock is installed');
        }
        // Advance clock so that the batch warmer executes once again and wait for the cache to be set
        // We disable the non-null assertion because we've already checked for existence in the line above
        await runAllUntil(this.clock, () => {
            const cacheSize = this.mockCache.cache.size;
            return cacheSize >= (expectedSize || 1);
        });
    }
    async getMetrics() {
        if (!this.metricsApi) {
            throw new Error('An attempt was made to fetch metrics, but the adapter was started without metrics enabled');
        }
        const response = await client.register.metrics();
        return new TestMetrics(response);
    }
    async getHealth() {
        return this.api.inject('/health');
    }
}
exports.TestAdapter = TestAdapter;
// This is the janky workaround to synchronously running async flows with fixed timers that block threads
async function waitUntilResolved(clock, fn) {
    let result;
    const execute = async () => {
        result = await fn();
    };
    execute();
    // eslint-disable-next-line no-unmodified-loop-condition
    while (result === undefined) {
        await clock.nextAsync();
    }
    return result;
}
function setEnvVariables(envVariables) {
    for (const key in envVariables) {
        process.env[key] = envVariables[key];
    }
}
/**
 * Sets the mocked websocket instance in the provided provider class.
 * We need this here, because the tests will connect using their instance of WebSocketClassProvider;
 * fetching from this library to the \@chainlink/ea-bootstrap package would access _another_ instance
 * of the same constructor. Although it should be a singleton, dependencies are different so that
 * means that the static classes themselves are also different.
 *
 * @param provider - singleton WebSocketClassProvider
 */
const mockWebSocketProvider = (provider) => {
    // Extend mock WebSocket class to bypass protocol headers error
    class MockWebSocket extends mock_socket_1.WebSocket {
        constructor(url, protocol) {
            super(url, protocol instanceof Object ? undefined : protocol);
        }
        // This is part of the 'ws' node library but not the common interface, but it's used in our WS transport
        removeAllListeners() {
            for (const eventType in this.listeners) {
                // We have to manually check because the mock-socket library shares this instance, and adds the server listeners to the same obj
                if (!eventType.startsWith('server')) {
                    delete this.listeners[eventType];
                }
            }
        }
    }
    // Need to disable typing, the mock-socket impl does not implement the ws interface fully
    provider.set(MockWebSocket); // eslint-disable-line @typescript-eslint/no-explicit-any
};
exports.mockWebSocketProvider = mockWebSocketProvider;
/* c8 ignore stop */ // eslint-disable-line
//# sourceMappingURL=testing-utils.js.map