"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const sinon_1 = __importDefault(require("sinon"));
const axios_1 = __importDefault(require("axios"));
const errors_1 = require("../../../src/providers/chain-state/external/adapters/errors");
const alchemy_1 = require("../../../src/providers/chain-state/external/adapters/alchemy");
const transaction_1 = require("../../../src/providers/chain-state/evm/models/transaction");
const config_1 = __importDefault(require("../../../src/config"));
// --- Mock data ---
// Use lowercase addresses so real Web3.utils.toChecksumAddress always accepts them
const VALID_TX_HASH = '0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1';
const VALID_ADDRESS = '0x742d35cc6634c0532925a3b844bc9e7595f2bd1e';
const VALID_FROM = '0x388c818ca8b9251b393131c08a736a67ccb19297';
const MOCK_TX = {
    hash: VALID_TX_HASH, blockNumber: '0x112a880', blockHash: '0xblockhash123',
    value: '0xde0b6b3a7640000', gas: '0x5208', gasPrice: '0x4a817c800',
    nonce: '0x5', to: VALID_ADDRESS, from: VALID_FROM, input: '0x', transactionIndex: '0x2a'
};
const MOCK_RECEIPT = {
    gasUsed: '0x5208', effectiveGasPrice: '0x4a817c800', status: '0x1',
    transactionHash: VALID_TX_HASH, blockNumber: '0x112a880', blockHash: '0xblockhash123'
};
const MOCK_BLOCK = { timestamp: '0x64f1f400', number: '0x112a880' };
function rpcOk(result) {
    return { status: 200, data: { jsonrpc: '2.0', id: 1, result } };
}
describe('AlchemyAdapter', function () {
    let sandbox;
    let adapter;
    let axiosPostStub;
    const savedExternalProviders = config_1.default.externalProviders;
    before(function () {
        config_1.default.externalProviders = { ...savedExternalProviders, alchemy: { apiKey: 'test-key' } };
    });
    after(function () {
        config_1.default.externalProviders = savedExternalProviders;
    });
    beforeEach(function () {
        sandbox = sinon_1.default.createSandbox();
        sandbox.stub(transaction_1.EVMTransactionStorage, 'addEffectsToTxs').callsFake(() => { });
        sandbox.stub(transaction_1.EVMTransactionStorage, 'abiDecode').returns(undefined);
        sandbox.stub(transaction_1.EVMTransactionStorage, '_apiTransform').callsFake((tx) => tx);
        axiosPostStub = sandbox.stub(axios_1.default, 'post');
        adapter = new alchemy_1.AlchemyAdapter({ name: 'alchemy', priority: 1 });
    });
    afterEach(function () { sandbox.restore(); });
    // --- Constructor ---
    describe('constructor', function () {
        it('should throw if apiKey is missing from config', function () {
            const saved = config_1.default.externalProviders;
            config_1.default.externalProviders = { alchemy: { apiKey: '' } };
            (0, chai_1.expect)(() => new alchemy_1.AlchemyAdapter({ name: 'alchemy', priority: 1 })).to.throw('apiKey is required');
            config_1.default.externalProviders = saved;
        });
        it('should set the adapter name', function () {
            (0, chai_1.expect)(adapter.name).to.equal('Alchemy');
        });
    });
    // --- getTransaction ---
    describe('getTransaction', function () {
        const params = { chain: 'ETH', network: 'mainnet', chainId: '1', txId: VALID_TX_HASH };
        it('should fetch tx + receipt + block and return transformed result', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk(MOCK_TX));
            axiosPostStub.onCall(1).resolves(rpcOk(MOCK_RECEIPT));
            axiosPostStub.onCall(2).resolves(rpcOk(MOCK_BLOCK));
            const result = await adapter.getTransaction(params);
            (0, chai_1.expect)(result).to.exist;
            (0, chai_1.expect)(result.txid).to.equal(VALID_TX_HASH);
            (0, chai_1.expect)(result.blockHeight).to.equal(18000000);
            (0, chai_1.expect)(result.nonce).to.equal('5');
            (0, chai_1.expect)(result.value).to.equal('1000000000000000000');
            (0, chai_1.expect)(result.gasPrice).to.equal('20000000000');
            (0, chai_1.expect)(result.data).to.equal('0x');
            (0, chai_1.expect)(axiosPostStub.callCount).to.equal(3);
        });
        it('should return undefined when tx not found', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk(null));
            axiosPostStub.onCall(1).resolves(rpcOk(null));
            (0, chai_1.expect)(await adapter.getTransaction(params)).to.be.undefined;
        });
        it('should return undefined for pending tx (blockNumber null)', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk({ ...MOCK_TX, blockNumber: null }));
            axiosPostStub.onCall(1).resolves(rpcOk(null));
            (0, chai_1.expect)(await adapter.getTransaction(params)).to.be.undefined;
        });
        it('should throw UPSTREAM error when receipt missing on confirmed tx', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk(MOCK_TX));
            axiosPostStub.onCall(1).resolves(rpcOk(null));
            try {
                await adapter.getTransaction(params);
                chai_1.expect.fail('Should have thrown');
            }
            catch (err) {
                (0, chai_1.expect)(err).to.be.instanceOf(errors_1.AdapterError);
                (0, chai_1.expect)(err.code).to.equal(errors_1.AdapterErrorCode.UPSTREAM);
            }
        });
        it('should throw INVALID_REQUEST for bad txId format', async function () {
            try {
                await adapter.getTransaction({ ...params, txId: 'bad' });
                chai_1.expect.fail('Should have thrown');
            }
            catch (err) {
                (0, chai_1.expect)(err).to.be.instanceOf(errors_1.AdapterError);
                (0, chai_1.expect)(err.code).to.equal(errors_1.AdapterErrorCode.INVALID_REQUEST);
                (0, chai_1.expect)(axiosPostStub.called).to.be.false;
            }
        });
        it('should derive the Alchemy URL dynamically for aliased chains', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk(null));
            axiosPostStub.onCall(1).resolves(rpcOk(null));
            await adapter.getTransaction({ ...params, chain: 'MATIC', network: 'amoy' });
            (0, chai_1.expect)(axiosPostStub.firstCall.args[0]).to.equal('https://polygon-amoy.g.alchemy.com/v2/test-key');
        });
        it('should use EIP-1559 effectiveGasPrice for fee calculation', async function () {
            const eip1559Tx = { ...MOCK_TX, gasPrice: '0x77359400' };
            const eip1559Receipt = { ...MOCK_RECEIPT, effectiveGasPrice: '0x3B9ACA00' };
            axiosPostStub.onCall(0).resolves(rpcOk(eip1559Tx));
            axiosPostStub.onCall(1).resolves(rpcOk(eip1559Receipt));
            axiosPostStub.onCall(2).resolves(rpcOk(MOCK_BLOCK));
            const result = await adapter.getTransaction(params);
            (0, chai_1.expect)(result.fee).to.equal(21000 * 1000000000);
        });
    });
    // --- Error classification ---
    describe('error classification via _jsonRpc', function () {
        const errorCases = [
            {
                scenario: 'HTTP 401 → AUTH',
                setup: () => axiosPostStub.rejects({ isAxiosError: true, response: { status: 401 } }),
                expectedCode: errors_1.AdapterErrorCode.AUTH
            },
            {
                scenario: 'HTTP 429 → RATE_LIMIT',
                setup: () => axiosPostStub.rejects({ isAxiosError: true, response: { status: 429 } }),
                expectedCode: errors_1.AdapterErrorCode.RATE_LIMIT
            },
            {
                scenario: 'HTTP 500 → UPSTREAM',
                setup: () => axiosPostStub.rejects({ isAxiosError: true, response: { status: 500 } }),
                expectedCode: errors_1.AdapterErrorCode.UPSTREAM
            },
            {
                scenario: 'timeout → TIMEOUT',
                setup: () => axiosPostStub.rejects({ isAxiosError: true, code: 'ECONNABORTED' }),
                expectedCode: errors_1.AdapterErrorCode.TIMEOUT
            },
            {
                scenario: 'JSON-RPC error -32602 → INVALID_REQUEST',
                setup: () => axiosPostStub.resolves({ status: 200, data: { jsonrpc: '2.0', id: 1, error: { code: -32602, message: 'invalid params' } } }),
                expectedCode: errors_1.AdapterErrorCode.INVALID_REQUEST
            },
            {
                scenario: 'JSON-RPC rate limit message → RATE_LIMIT',
                setup: () => axiosPostStub.resolves({ status: 200, data: { jsonrpc: '2.0', id: 1, error: { code: -32000, message: 'rate limit exceeded' } } }),
                expectedCode: errors_1.AdapterErrorCode.RATE_LIMIT
            },
        ];
        const params = { chain: 'ETH', network: 'mainnet', chainId: '1', txId: VALID_TX_HASH };
        for (const { scenario, setup, expectedCode } of errorCases) {
            it(`should classify ${scenario}`, async function () {
                setup();
                try {
                    await adapter.getTransaction(params);
                    chai_1.expect.fail('Should have thrown');
                }
                catch (err) {
                    (0, chai_1.expect)(err).to.be.instanceOf(errors_1.AdapterError);
                    (0, chai_1.expect)(err.code).to.equal(expectedCode);
                }
            });
        }
    });
    // --- Asset transfer stream ---
    describe('AlchemyAssetTransferStream', function () {
        it('should query both fromAddress and toAddress and deduplicate', async function () {
            const transfer1 = { hash: '0x1'.padEnd(66, '0'), blockNum: '0x1', from: VALID_FROM, to: VALID_ADDRESS, value: 1, category: 'external', uniqueId: 'u1', metadata: { blockTimestamp: '2023-01-01T00:00:00Z' } };
            const transfer2 = { ...transfer1, uniqueId: 'u2', hash: '0x2'.padEnd(66, '0') };
            axiosPostStub.onCall(0).resolves({ status: 200, data: { result: { transfers: [transfer1], pageKey: null } } });
            axiosPostStub.onCall(1).resolves({ status: 200, data: { result: { transfers: [transfer1, transfer2], pageKey: null } } });
            const stream = adapter.streamAddressTransactions({
                chain: 'ETH', network: 'mainnet', chainId: '1', address: VALID_ADDRESS,
                args: { startBlock: 0, endBlock: 100 }
            });
            const items = [];
            await new Promise((resolve, reject) => {
                stream.on('data', (d) => items.push(d));
                stream.on('end', resolve);
                stream.on('error', reject);
            });
            (0, chai_1.expect)(items).to.have.length(2);
        });
        it('should emit INVALID_REQUEST error for invalid address', function (done) {
            const stream = new alchemy_1.AlchemyAssetTransferStream('https://example.com', { chain: 'ETH', network: 'mainnet', address: 'bad-address', args: {} }, (t) => t);
            stream.on('error', (err) => {
                (0, chai_1.expect)(err).to.be.instanceOf(errors_1.AdapterError);
                (0, chai_1.expect)(err.code).to.equal(errors_1.AdapterErrorCode.INVALID_REQUEST);
                done();
            });
        });
    });
    // --- getBlockNumberByDate ---
    describe('getBlockNumberByDate', function () {
        it('should binary search for block closest to target date', async function () {
            axiosPostStub.callsFake(async (_url, body) => {
                if (body.method === 'eth_blockNumber')
                    return rpcOk('0x64');
                if (body.method === 'eth_getBlockByNumber') {
                    const num = parseInt(body.params[0], 16);
                    return rpcOk({ timestamp: `0x${num.toString(16)}`, number: body.params[0] });
                }
                return rpcOk(null);
            });
            const result = await adapter.getBlockNumberByDate({ chain: 'ETH', network: 'mainnet', chainId: '1', date: new Date(50000) });
            (0, chai_1.expect)(result).to.equal(50);
        });
        it('should return latest block if target is in the future', async function () {
            axiosPostStub.onCall(0).resolves(rpcOk('0x64'));
            axiosPostStub.onCall(1).resolves(rpcOk({ timestamp: '0x64', number: '0x64' }));
            const result = await adapter.getBlockNumberByDate({ chain: 'ETH', network: 'mainnet', chainId: '1', date: new Date(200000) });
            (0, chai_1.expect)(result).to.equal(100);
        });
    });
    // --- healthCheck ---
    describe('healthCheck', function () {
        it('should return true on successful eth_blockNumber call', async function () {
            axiosPostStub.resolves(rpcOk('0x1'));
            (0, chai_1.expect)(await adapter.healthCheck()).to.equal(true);
        });
        it('should return false on failure', async function () {
            axiosPostStub.rejects(new Error('network error'));
            (0, chai_1.expect)(await adapter.healthCheck()).to.equal(false);
        });
    });
});
//# sourceMappingURL=alchemy.test.js.map