// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Test.sol";

import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
import "./utils/WormholeTestUtils.t.sol";
import "./utils/PythTestUtils.t.sol";

contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
    // 19, current mainnet number of guardians, is used to have gas estimates
    // close to our mainnet transactions.
    uint8 constant NUM_GUARDIANS = 19;
    // 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
    // It is possible to have more signers but the median seems to be 13.
    uint8 constant NUM_GUARDIAN_SIGNERS = 13;

    // We use 5 prices to form a batch of 5 prices, close to our mainnet transactions.
    uint8 constant NUM_PRICES = 5;

    // We will have less than 512 price for a foreseeable future.
    uint8 constant MERKLE_TREE_DEPTH = 9;

    IWormhole public wormhole;
    IPyth public pyth;

    bytes32[] priceIds;

    // Cached prices are populated in the setUp
    PythStructs.Price[] cachedPrices;
    uint64[] cachedPricesPublishTimes;

    bytes[][] cachedPricesUpdateData; // i th element contains the update data for the first i prices
    bytes[] allCachedPricesUpdateData; // the update data for all prices
    uint[] cachedPricesUpdateFee; // i th element contains the update fee for the first i prices
    uint allCachedPricesUpdateFee; // the update fee for all prices

    // Fresh prices are different prices that can be used
    // as a fresh price to update the prices
    PythStructs.Price[] freshPrices;
    uint64[] freshPricesPublishTimes;
    bytes[][] freshPricesUpdateData; // i th element contains the update data for the first i prices
    bytes[] allFreshPricesUpdateData; // the update data for all prices
    uint[] freshPricesUpdateFee; // i th element contains the update fee for the first i prices
    uint allFreshPricesUpdateFee; // the update fee for all prices

    uint64 sequence;
    uint randomSeed;

    function setUp() public {
        address wormholeAddr = setUpWormholeReceiver(NUM_GUARDIANS);
        wormhole = IWormhole(wormholeAddr);
        pyth = IPyth(setUpPyth(wormholeAddr));

        priceIds = new bytes32[](NUM_PRICES);
        priceIds[0] = bytes32(
            0x1000000000000000000000000000000000000000000000000000000000000f00
        );
        for (uint i = 1; i < NUM_PRICES; ++i) {
            priceIds[i] = bytes32(uint256(priceIds[i - 1]) + 1);
        }

        for (uint i = 0; i < NUM_PRICES; ++i) {
            uint64 publishTime = uint64(getRand() % 10) + 1; // to make sure prevPublishTime is >= 0

            cachedPrices.push(
                PythStructs.Price(
                    int64(uint64(getRand() % 1000)), // Price
                    uint64(getRand() % 100), // Confidence
                    -5, // Expo
                    publishTime
                )
            );
            cachedPricesPublishTimes.push(publishTime);

            publishTime += uint64(getRand() % 10);
            freshPrices.push(
                PythStructs.Price(
                    int64(uint64(getRand() % 1000)), // Price
                    uint64(getRand() % 100), // Confidence
                    -5, // Expo
                    publishTime
                )
            );
            freshPricesPublishTimes.push(publishTime);

            // Generate Wormhole Merkle update data and fee for the first i th prices
            (
                bytes[] memory updateData,
                uint updateFee
            ) = generateUpdateDataAndFee(cachedPrices);

            cachedPricesUpdateData.push(updateData);
            cachedPricesUpdateFee.push(updateFee);

            (updateData, updateFee) = generateUpdateDataAndFee(freshPrices);

            freshPricesUpdateData.push(updateData);
            freshPricesUpdateFee.push(updateFee);
        }
        allCachedPricesUpdateData = cachedPricesUpdateData[NUM_PRICES - 1];
        allCachedPricesUpdateFee = cachedPricesUpdateFee[NUM_PRICES - 1];
        allFreshPricesUpdateData = freshPricesUpdateData[NUM_PRICES - 1];
        allFreshPricesUpdateFee = freshPricesUpdateFee[NUM_PRICES - 1];

        // Populate the contract with the initial prices
        pyth.updatePriceFeeds{value: allCachedPricesUpdateFee}(
            allCachedPricesUpdateData
        );
    }

    function getRand() internal returns (uint val) {
        ++randomSeed;
        val = uint(keccak256(abi.encode(randomSeed)));
    }

    function generateUpdateDataAndFee(
        PythStructs.Price[] memory prices
    ) internal returns (bytes[] memory updateData, uint updateFee) {
        updateData = new bytes[](1);

        updateData[0] = generateWhMerkleUpdate(
            pricesToPriceFeedMessages(priceIds, prices),
            MERKLE_TREE_DEPTH,
            NUM_GUARDIAN_SIGNERS
        );

        updateFee = pyth.getUpdateFee(updateData);
    }

    function testBenchmarkUpdatePriceFeeds1FeedFresh() public {
        pyth.updatePriceFeeds{value: freshPricesUpdateFee[0]}(
            freshPricesUpdateData[0]
        );
    }

    function testBenchmarkUpdatePriceFeeds2FeedsFresh() public {
        pyth.updatePriceFeeds{value: freshPricesUpdateFee[1]}(
            freshPricesUpdateData[1]
        );
    }

    function testBenchmarkUpdatePriceFeeds3FeedsFresh() public {
        pyth.updatePriceFeeds{value: freshPricesUpdateFee[2]}(
            freshPricesUpdateData[2]
        );
    }

    function testBenchmarkUpdatePriceFeeds4FeedsFresh() public {
        pyth.updatePriceFeeds{value: freshPricesUpdateFee[3]}(
            freshPricesUpdateData[3]
        );
    }

    function testBenchmarkUpdatePriceFeeds5FeedsFresh() public {
        pyth.updatePriceFeeds{value: freshPricesUpdateFee[4]}(
            freshPricesUpdateData[4]
        );
    }

    function testBenchmarkUpdatePriceFeeds1FeedNotFresh() public {
        pyth.updatePriceFeeds{value: cachedPricesUpdateFee[0]}(
            cachedPricesUpdateData[0]
        );
    }

    function testBenchmarkUpdatePriceFeeds2FeedsNotFresh() public {
        pyth.updatePriceFeeds{value: cachedPricesUpdateFee[1]}(
            cachedPricesUpdateData[1]
        );
    }

    function testBenchmarkUpdatePriceFeeds3FeedsNotFresh() public {
        pyth.updatePriceFeeds{value: cachedPricesUpdateFee[2]}(
            cachedPricesUpdateData[2]
        );
    }

    function testBenchmarkUpdatePriceFeeds4FeedsNotFresh() public {
        pyth.updatePriceFeeds{value: cachedPricesUpdateFee[3]}(
            cachedPricesUpdateData[3]
        );
    }

    function testBenchmarkUpdatePriceFeeds5FeedsNotFresh() public {
        pyth.updatePriceFeeds{value: cachedPricesUpdateFee[4]}(
            cachedPricesUpdateData[4]
        );
    }

    function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public {
        // Since the prices have advanced, the publishTimes are newer than one in
        // the contract and hence, the call should succeed.
        pyth.updatePriceFeedsIfNecessary{value: allFreshPricesUpdateFee}(
            allFreshPricesUpdateData,
            priceIds,
            freshPricesPublishTimes
        );
    }

    function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public {
        // Since the price is not advanced, the publishTimes are the same as the
        // ones in the contract.
        vm.expectRevert(PythErrors.NoFreshUpdate.selector);

        pyth.updatePriceFeedsIfNecessary{value: allCachedPricesUpdateFee}(
            allCachedPricesUpdateData,
            priceIds,
            cachedPricesPublishTimes
        );
    }

    function testBenchmarkParsePriceFeedUpdatesForOnePriceFeed() public {
        bytes32[] memory ids = new bytes32[](1);
        ids[0] = priceIds[0];

        pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}(
            allFreshPricesUpdateData,
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdatesForTwoPriceFeed() public {
        bytes32[] memory ids = new bytes32[](2);
        ids[0] = priceIds[0];
        ids[1] = priceIds[1];

        pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}(
            allFreshPricesUpdateData,
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdatesUniqueFor() public {
        bytes32[] memory ids = new bytes32[](1);
        ids[0] = priceIds[0];

        pyth.parsePriceFeedUpdatesUnique{value: freshPricesUpdateFee[0]}(
            freshPricesUpdateData[0],
            ids,
            uint64(freshPrices[0].publishTime),
            100
        );
    }

    function testBenchmarkParsePriceFeedUpdatesUniqueForOnePriceFeedNotWithinRange()
        public
    {
        bytes32[] memory ids = new bytes32[](1);
        ids[0] = priceIds[0];

        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdatesUnique{value: freshPricesUpdateFee[0]}(
            freshPricesUpdateData[0],
            ids,
            uint64(freshPrices[0].publishTime) - 1,
            100
        );
    }

    function testBenchmarkParsePriceFeedUpdates1() public {
        uint numIds = 1;

        bytes32[] memory ids = new bytes32[](numIds);
        for (uint i = 0; i < numIds; i++) {
            ids[i] = priceIds[i];
        }
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}(
            freshPricesUpdateData[numIds - 1],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdates2() public {
        uint numIds = 2;

        bytes32[] memory ids = new bytes32[](numIds);
        for (uint i = 0; i < numIds; i++) {
            ids[i] = priceIds[i];
        }
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}(
            freshPricesUpdateData[numIds - 1],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdates3() public {
        uint numIds = 3;

        bytes32[] memory ids = new bytes32[](numIds);
        for (uint i = 0; i < numIds; i++) {
            ids[i] = priceIds[i];
        }
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}(
            freshPricesUpdateData[numIds - 1],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdates4() public {
        uint numIds = 4;

        bytes32[] memory ids = new bytes32[](numIds);
        for (uint i = 0; i < numIds; i++) {
            ids[i] = priceIds[i];
        }
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}(
            freshPricesUpdateData[numIds - 1],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdates5() public {
        uint numIds = 5;

        bytes32[] memory ids = new bytes32[](numIds);
        for (uint i = 0; i < numIds; i++) {
            ids[i] = priceIds[i];
        }
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}(
            freshPricesUpdateData[numIds - 1],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdatesForAllPriceFeedsShuffledSubsetPriceIds()
        public
    {
        uint numIds = 3;
        bytes32[] memory ids = new bytes32[](numIds);
        ids[0] = priceIds[4];
        ids[1] = priceIds[2];
        ids[2] = priceIds[0];
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[4]}( // updateFee based on number of priceFeeds in updateData
            freshPricesUpdateData[4],
            ids,
            0,
            50
        );
    }

    function testBenchmarkParsePriceFeedUpdatesForOnePriceFeedNotWithinRange()
        public
    {
        bytes32[] memory ids = new bytes32[](1);
        ids[0] = priceIds[0];

        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[0]}(
            freshPricesUpdateData[0],
            ids,
            50,
            100
        );
    }

    function testBenchmarkParsePriceFeedUpdatesForAllPriceFeedsNotWithinRange()
        public
    {
        bytes32[] memory ids = new bytes32[](1);
        ids[0] = priceIds[0];

        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}(
            allFreshPricesUpdateData,
            ids,
            50,
            100
        );
    }

    function testBenchmarkGetPrice() public {
        // Set the block timestamp to 0. As prices have < 10 timestamp and staleness
        // is set to 60 seconds, the getPrice should work as expected.
        vm.warp(0);

        pyth.getPrice(priceIds[0]);
    }

    function testBenchmarkGetEmaPrice() public {
        // Set the block timestamp to 0. As prices have < 10 timestamp and staleness
        // is set to 60 seconds, the getPrice should work as expected.
        vm.warp(0);

        pyth.getEmaPrice(priceIds[0]);
    }

    function testBenchmarkGetUpdateFee1() public view {
        pyth.getUpdateFee(freshPricesUpdateData[0]);
    }

    function testBenchmarkGetUpdateFee2() public view {
        pyth.getUpdateFee(freshPricesUpdateData[1]);
    }

    function testBenchmarkGetUpdateFee3() public view {
        pyth.getUpdateFee(freshPricesUpdateData[2]);
    }

    function testBenchmarkGetUpdateFee4() public view {
        pyth.getUpdateFee(freshPricesUpdateData[3]);
    }

    function testBenchmarkGetUpdateFee5() public view {
        pyth.getUpdateFee(freshPricesUpdateData[4]);
    }
}
