"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 });
const chai_1 = require("chai");
const sinon = __importStar(require("sinon"));
const p2p_1 = require("../../../../src/modules/bitcoin/p2p");
const chain_state_1 = require("../../../../src/providers/chain-state");
const state_1 = require("../../../../src/models/state");
const libs_1 = require("../../../../src/providers/libs");
const unit_1 = require("../../../helpers/unit");
describe('BitcoinP2PWorker', function () {
    const sandbox = sinon.createSandbox();
    const chain = 'BTC';
    const network = 'regtest';
    before(unit_1.unitBeforeHelper);
    after(unit_1.unitAfterHelper);
    afterEach(() => sandbox.restore());
    function createWorker(overrides = {}) {
        const mockBitcoreLib = {
            Networks: { get: sandbox.stub().returns('regtest') },
            encoding: { BufferReader: sandbox.stub() },
            Block: sandbox.stub().callsFake((buf) => ({
                hash: buf.toString('hex').substring(0, 8),
                header: { toObject: () => ({ hash: 'testhash', prevHash: '', time: Date.now() / 1000 }) },
                transactions: []
            }))
        };
        const mockMessages = function () { };
        mockMessages.prototype = {};
        const mockPool = function () { };
        mockPool.prototype = { on: sandbox.stub(), connect: sandbox.stub(), once: sandbox.stub() };
        const mockInventory = { TYPE: { BLOCK: 1, TX: 2 } };
        const mockBitcoreP2p = {
            Messages: mockMessages,
            Pool: mockPool,
            Inventory: mockInventory
        };
        sandbox.stub(libs_1.Libs, 'get').returns({ lib: mockBitcoreLib, p2p: mockBitcoreP2p });
        const chainConfig = {
            trustedPeers: [],
            parentChain: undefined,
            forkHeight: undefined,
            rpc: { host: 'localhost', port: 8332, username: 'test', password: 'test' },
            ...overrides
        };
        return new p2p_1.BitcoinP2PWorker({ chain, network, chainConfig });
    }
    function setupSyncStubs(tipHeight = 100) {
        sandbox.stub(state_1.StateStorage, 'collection').value({
            findOne: sandbox.stub().resolves({ initialSyncComplete: [`${chain}:${network}`] }),
            findOneAndUpdate: sandbox.stub().resolves()
        });
        sandbox.stub(chain_state_1.ChainStateProvider, 'getLocalTip').resolves({ height: tipHeight });
        sandbox.stub(chain_state_1.ChainStateProvider, 'getLocatorHashes').resolves([Array(65).join('0')]);
    }
    describe('useMultiThread()', function () {
        it('should return true when CPU cores > 2 and threads not configured', function () {
            const worker = createWorker();
            // eslint-disable-next-line @typescript-eslint/no-require-imports
            const cpuStub = sandbox.stub(require('os'), 'cpus');
            cpuStub.returns(new Array(8)); // 8 cores
            (0, chai_1.expect)(worker.useMultiThread()).to.equal(true);
        });
        it('should return false when threads is 0', function () {
            const worker = createWorker({ threads: 0 });
            (0, chai_1.expect)(worker.useMultiThread()).to.equal(false);
        });
        it('should return true when threads is > 0', function () {
            const worker = createWorker({ threads: 4 });
            (0, chai_1.expect)(worker.useMultiThread()).to.equal(true);
        });
    });
    describe('sync() with prefetching (single-thread mode)', function () {
        it('should prefetch blocks in parallel during sync', async function () {
            const worker = createWorker({ prefetchSize: 3, threads: 0 });
            const getBlockStartTimes = [];
            const processOrder = [];
            const blockData = {};
            for (let i = 0; i < 5; i++) {
                const hash = `hash_${i}`;
                blockData[hash] = { hash, header: { toObject: () => ({ hash, prevHash: `hash_${i - 1}`, time: Date.now() / 1000 }) }, transactions: [] };
            }
            const headers = Object.keys(blockData).map(hash => ({ hash }));
            sandbox.stub(worker, 'getBlock').callsFake(async (hash) => {
                getBlockStartTimes.push({ hash, time: Date.now() });
                await new Promise(r => setTimeout(r, 50));
                return blockData[hash];
            });
            sandbox.stub(worker, 'processBlock').callsFake(async (block) => {
                processOrder.push(block.hash);
                await new Promise(r => setTimeout(r, 10));
            });
            setupSyncStubs();
            let headerCallCount = 0;
            sandbox.stub(worker, 'getHeadersForSync').callsFake(async () => {
                headerCallCount++;
                return headerCallCount === 1 ? headers : [];
            });
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            // Blocks processed in sequential order
            (0, chai_1.expect)(processOrder).to.deep.equal(['hash_0', 'hash_1', 'hash_2', 'hash_3', 'hash_4']);
            // First 3 getBlock calls started nearly simultaneously (prefetched)
            (0, chai_1.expect)(getBlockStartTimes.length).to.equal(5);
            const firstThreeStartTimes = getBlockStartTimes.slice(0, 3).map(t => t.time);
            const timeDiff = Math.max(...firstThreeStartTimes) - Math.min(...firstThreeStartTimes);
            (0, chai_1.expect)(timeDiff).to.be.lessThan(20);
        });
        it('should process blocks sequentially even with prefetch', async function () {
            const worker = createWorker({ prefetchSize: 5, threads: 0 });
            const processOrder = [];
            const headers = Array.from({ length: 5 }, (_, i) => ({ hash: `block_${i}` }));
            sandbox.stub(worker, 'getBlock').callsFake(async (hash) => {
                const idx = parseInt(hash.split('_')[1]);
                // Later blocks resolve faster to test ordering
                await new Promise(r => setTimeout(r, (5 - idx) * 20));
                return { hash, header: { toObject: () => ({ hash, prevHash: '', time: Date.now() / 1000 }) }, transactions: [] };
            });
            sandbox.stub(worker, 'processBlock').callsFake(async (block) => {
                const idx = parseInt(block.hash.split('_')[1]);
                processOrder.push(idx);
            });
            setupSyncStubs();
            let headerCallCount = 0;
            sandbox.stub(worker, 'getHeadersForSync').callsFake(async () => {
                headerCallCount++;
                return headerCallCount === 1 ? headers : [];
            });
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            (0, chai_1.expect)(processOrder).to.deep.equal([0, 1, 2, 3, 4]);
        });
        it('should work with prefetchSize=0 (disabled)', async function () {
            const worker = createWorker({ prefetchSize: 0, threads: 0 });
            const getBlockCalls = [];
            const headers = Array.from({ length: 3 }, (_, i) => ({ hash: `blk_${i}` }));
            sandbox.stub(worker, 'getBlock').callsFake(async (hash) => {
                getBlockCalls.push(Date.now());
                return { hash, header: { toObject: () => ({ hash, prevHash: '', time: Date.now() / 1000 }) }, transactions: [] };
            });
            sandbox.stub(worker, 'processBlock').callsFake(async () => {
                await new Promise(r => setTimeout(r, 30));
            });
            setupSyncStubs();
            let headerCallCount = 0;
            sandbox.stub(worker, 'getHeadersForSync').callsFake(async () => {
                headerCallCount++;
                return headerCallCount === 1 ? headers : [];
            });
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            // Sequential: each getBlock starts after previous processBlock completes
            for (let i = 1; i < getBlockCalls.length; i++) {
                (0, chai_1.expect)(getBlockCalls[i] - getBlockCalls[i - 1]).to.be.at.least(25);
            }
        });
        it('should use default prefetchSize of 10 when not configured', async function () {
            const worker = createWorker({ threads: 0 });
            let concurrentFetches = 0;
            let maxConcurrentFetches = 0;
            const headers = Array.from({ length: 15 }, (_, i) => ({ hash: `h_${i}` }));
            sandbox.stub(worker, 'getBlock').callsFake(async (hash) => {
                concurrentFetches++;
                maxConcurrentFetches = Math.max(maxConcurrentFetches, concurrentFetches);
                await new Promise(r => setTimeout(r, 20));
                concurrentFetches--;
                return { hash, header: { toObject: () => ({ hash, prevHash: '', time: Date.now() / 1000 }) }, transactions: [] };
            });
            sandbox.stub(worker, 'processBlock').callsFake(async () => {
                await new Promise(r => setTimeout(r, 5));
            });
            setupSyncStubs();
            let headerCallCount = 0;
            sandbox.stub(worker, 'getHeadersForSync').callsFake(async () => {
                headerCallCount++;
                return headerCallCount === 1 ? headers : [];
            });
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            (0, chai_1.expect)(maxConcurrentFetches).to.be.greaterThan(1);
            (0, chai_1.expect)(maxConcurrentFetches).to.be.at.most(10);
        });
    });
    describe('sync() dual-mode behavior', function () {
        it('should delegate to multiThreadSync when initial sync not complete and threads > 0', async function () {
            const worker = createWorker({ threads: 4 });
            const mtSyncStub = sandbox.stub(worker.multiThreadSync, 'sync').resolves(true);
            // initialSyncComplete = false
            sandbox.stub(state_1.StateStorage, 'collection').value({
                findOne: sandbox.stub().resolves({ initialSyncComplete: [] }),
                findOneAndUpdate: sandbox.stub().resolves()
            });
            sandbox.stub(chain_state_1.ChainStateProvider, 'getLocalTip').resolves({ height: 100 });
            sandbox.stub(chain_state_1.ChainStateProvider, 'getLocatorHashes').resolves([Array(65).join('0')]);
            sandbox.stub(worker, 'getHeadersForSync').resolves([]);
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            (0, chai_1.expect)(mtSyncStub.calledOnce).to.equal(true);
        });
        it('should use single-thread sync when threads=0 even if initial sync not complete', async function () {
            const worker = createWorker({ threads: 0, prefetchSize: 0 });
            const mtSyncStub = sandbox.stub(worker.multiThreadSync, 'sync').resolves(true);
            sandbox.stub(state_1.StateStorage, 'collection').value({
                findOne: sandbox.stub().resolves({ initialSyncComplete: [] }),
                findOneAndUpdate: sandbox.stub().resolves()
            });
            sandbox.stub(chain_state_1.ChainStateProvider, 'getLocalTip').resolves({ height: 100 });
            sandbox.stub(chain_state_1.ChainStateProvider, 'getLocatorHashes').resolves([Array(65).join('0')]);
            sandbox.stub(worker, 'getHeadersForSync').resolves([]);
            sandbox.stub(worker, 'getBlock').resolves({});
            sandbox.stub(worker, 'processBlock').resolves();
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            (0, chai_1.expect)(mtSyncStub.called).to.equal(false);
        });
        it('should use single-thread sync when initial sync is already complete', async function () {
            const worker = createWorker({ threads: 4 });
            const mtSyncStub = sandbox.stub(worker.multiThreadSync, 'sync').resolves(true);
            // Set the in-memory flag directly (simulates INITIALSYNCDONE event having fired)
            worker.initialSyncComplete = true;
            setupSyncStubs();
            let headerCallCount = 0;
            sandbox.stub(worker, 'getHeadersForSync').callsFake(async () => {
                headerCallCount++;
                return [];
            });
            worker.isSyncingNode = true;
            worker.isSyncing = false;
            await worker.sync();
            (0, chai_1.expect)(mtSyncStub.called).to.equal(false);
        });
    });
});
//# sourceMappingURL=p2p.test.js.map