// 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";
import "./utils/RandTestUtils.t.sol";

import "../contracts/libraries/MerkleTree.sol";

contract PythWormholeMerkleAccumulatorTest is
    Test,
    WormholeTestUtils,
    PythTestUtils
{
    IPyth public pyth;

    // -1 is equal to 0xffffff which is the biggest uint if converted back
    uint64 constant MAX_UINT64 = uint64(int64(-1));

    function setUp() public virtual {
        pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
    }

    function assertPriceFeedMessageStored(
        PriceFeedMessage memory priceFeedMessage
    ) internal {
        PythStructs.Price memory aggregatePrice = pyth.getPriceUnsafe(
            priceFeedMessage.priceId
        );
        assertEq(aggregatePrice.price, priceFeedMessage.price);
        assertEq(aggregatePrice.conf, priceFeedMessage.conf);
        assertEq(aggregatePrice.expo, priceFeedMessage.expo);
        assertEq(aggregatePrice.publishTime, priceFeedMessage.publishTime);

        PythStructs.Price memory emaPrice = pyth.getEmaPriceUnsafe(
            priceFeedMessage.priceId
        );
        assertEq(emaPrice.price, priceFeedMessage.emaPrice);
        assertEq(emaPrice.conf, priceFeedMessage.emaConf);
        assertEq(emaPrice.expo, priceFeedMessage.expo);
        assertEq(emaPrice.publishTime, priceFeedMessage.publishTime);
    }

    function assertParsedPriceFeedEqualsMessage(
        PythStructs.PriceFeed memory priceFeed,
        PriceFeedMessage memory priceFeedMessage,
        bytes32 priceId
    ) internal {
        assertEq(priceFeed.id, priceId);
        assertEq(priceFeed.price.price, priceFeedMessage.price);
        assertEq(priceFeed.price.conf, priceFeedMessage.conf);
        assertEq(priceFeed.price.expo, priceFeedMessage.expo);
        assertEq(priceFeed.price.publishTime, priceFeedMessage.publishTime);
        assertEq(priceFeed.emaPrice.price, priceFeedMessage.emaPrice);
        assertEq(priceFeed.emaPrice.conf, priceFeedMessage.emaConf);
        assertEq(priceFeed.emaPrice.expo, priceFeedMessage.expo);
        assertEq(priceFeed.emaPrice.publishTime, priceFeedMessage.publishTime);
    }

    function assertParsedPriceFeedStored(
        PythStructs.PriceFeed memory priceFeed
    ) internal {
        PythStructs.Price memory aggregatePrice = pyth.getPriceUnsafe(
            priceFeed.id
        );
        assertEq(aggregatePrice.price, priceFeed.price.price);
        assertEq(aggregatePrice.conf, priceFeed.price.conf);
        assertEq(aggregatePrice.expo, priceFeed.price.expo);
        assertEq(aggregatePrice.publishTime, priceFeed.price.publishTime);

        PythStructs.Price memory emaPrice = pyth.getEmaPriceUnsafe(
            priceFeed.id
        );
        assertEq(emaPrice.price, priceFeed.emaPrice.price);
        assertEq(emaPrice.conf, priceFeed.emaPrice.conf);
        assertEq(emaPrice.expo, priceFeed.emaPrice.expo);
        assertEq(emaPrice.publishTime, priceFeed.emaPrice.publishTime);
    }

    function assertPriceFeedEqual(
        PythStructs.PriceFeed memory priceFeed1,
        PythStructs.PriceFeed memory priceFeed2
    ) internal {
        assertEq(priceFeed1.id, priceFeed2.id);
        assertEq(priceFeed1.price.price, priceFeed2.price.price);
        assertEq(priceFeed1.price.conf, priceFeed2.price.conf);
        assertEq(priceFeed1.price.expo, priceFeed2.price.expo);
        assertEq(priceFeed1.price.publishTime, priceFeed2.price.publishTime);
        assertEq(priceFeed1.emaPrice.price, priceFeed2.emaPrice.price);
        assertEq(priceFeed1.emaPrice.conf, priceFeed2.emaPrice.conf);
        assertEq(priceFeed1.emaPrice.expo, priceFeed2.emaPrice.expo);
        assertEq(
            priceFeed1.emaPrice.publishTime,
            priceFeed2.emaPrice.publishTime
        );
    }

    function generateRandomPriceFeedMessage(
        uint numPriceFeeds
    ) internal returns (PriceFeedMessage[] memory priceFeedMessages) {
        priceFeedMessages = new PriceFeedMessage[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i] = PriceFeedMessage({
                priceId: getRandBytes32(),
                price: getRandInt64(),
                conf: getRandUint64(),
                expo: getRandInt32(),
                publishTime: getRandUint64(),
                prevPublishTime: getRandUint64(),
                emaPrice: getRandInt64(),
                emaConf: getRandUint64()
            });
        }
    }

    /**
     * @notice Returns `numPriceFeeds` random price feed messages with price & expo bounded
     * to realistic values and publishTime set to 1.
     */
    function generateRandomBoundedPriceFeedMessage(
        uint numPriceFeeds
    ) internal returns (PriceFeedMessage[] memory priceFeedMessages) {
        priceFeedMessages = new PriceFeedMessage[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i] = PriceFeedMessage({
                priceId: getRandBytes32(),
                price: int64(getRandUint64() / 10), // assuming price should always be positive
                conf: getRandUint64(),
                expo: int32(getRandInt8() % 13), // pyth contract guarantees that expo between [-12, 12]
                publishTime: uint64(1),
                prevPublishTime: getRandUint64(),
                emaPrice: getRandInt64(),
                emaConf: getRandUint64()
            });
        }
    }

    function createWormholeMerkleUpdateData(
        PriceFeedMessage[] memory priceFeedMessages
    ) internal returns (bytes[] memory updateData, uint updateFee) {
        updateData = new bytes[](1);

        uint8 depth = 0;
        while ((1 << depth) < priceFeedMessages.length) {
            depth++;
        }

        depth += getRandUint8() % 3;

        updateData[0] = generateWhMerkleUpdate(priceFeedMessages, depth, 1);

        updateFee = pyth.getUpdateFee(updateData);
    }

    /// @notice This method creates a forward compatible wormhole update data by using a newer minor version,
    /// setting a trailing header size and generating additional trailing header data of size `trailingHeaderSize`
    function createFowardCompatibleWormholeMerkleUpdateData(
        PriceFeedMessage[] memory priceFeedMessages,
        uint8 minorVersion,
        uint8 trailingHeaderSize
    ) internal returns (bytes[] memory updateData, uint updateFee) {
        updateData = new bytes[](1);

        uint8 depth = 0;
        while ((1 << depth) < priceFeedMessages.length) {
            depth++;
        }

        depth += getRandUint8() % 3;
        bytes memory trailingHeaderData = new bytes(uint8(0));
        for (uint i = 0; i < trailingHeaderSize; i++) {
            trailingHeaderData = abi.encodePacked(trailingHeaderData, uint8(i));
        }
        updateData[0] = generateForwardCompatibleWhMerkleUpdate(
            priceFeedMessages,
            depth,
            1,
            minorVersion,
            trailingHeaderData
        );

        updateFee = pyth.getUpdateFee(updateData);
    }

    /// Testing update price feeds method using wormhole merkle update type.
    function testUpdatePriceFeedWithWormholeMerkleWorks(uint seed) public {
        setRandSeed(seed);

        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);

        pyth.updatePriceFeeds{value: updateFee}(updateData);

        for (uint i = 0; i < numPriceFeeds; i++) {
            assertPriceFeedMessageStored(priceFeedMessages[i]);
        }

        // Update the prices again with the same data should work
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        for (uint i = 0; i < numPriceFeeds; i++) {
            assertPriceFeedMessageStored(priceFeedMessages[i]);
        }

        // Update the prices again with updated data should update the prices
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i].price = getRandInt64();
            priceFeedMessages[i].conf = getRandUint64();
            priceFeedMessages[i].expo = getRandInt32();

            // Increase the publish time if it is not causing an overflow
            if (priceFeedMessages[i].publishTime != type(uint64).max) {
                priceFeedMessages[i].publishTime += 1;
            }
            priceFeedMessages[i].emaPrice = getRandInt64();
            priceFeedMessages[i].emaConf = getRandUint64();
        }

        (updateData, updateFee) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        pyth.updatePriceFeeds{value: updateFee}(updateData);

        for (uint i = 0; i < numPriceFeeds; i++) {
            assertPriceFeedMessageStored(priceFeedMessages[i]);
        }
    }

    function testUpdatePriceFeedWithWormholeMerkleWorksOnMultiUpdate() public {
        PriceFeedMessage[]
            memory priceFeedMessages1 = generateRandomPriceFeedMessage(2);
        PriceFeedMessage[]
            memory priceFeedMessages2 = generateRandomPriceFeedMessage(2);

        // Make the 2nd message of the second update the same as the 1st message of the first update
        priceFeedMessages2[1].priceId = priceFeedMessages1[0].priceId;
        // Adjust the timestamps so the second timestamp is greater than the first
        priceFeedMessages1[0].publishTime = 5;
        priceFeedMessages2[1].publishTime = 10;

        bytes[] memory updateData = new bytes[](2);

        uint8 depth = 1; // 2 messages
        uint8 numSigners = 1;
        updateData[0] = generateWhMerkleUpdate(
            priceFeedMessages1,
            depth,
            numSigners
        );
        updateData[1] = generateWhMerkleUpdate(
            priceFeedMessages2,
            depth,
            numSigners
        );

        uint updateFee = pyth.getUpdateFee(updateData);

        bytes32[] memory priceIds = new bytes32[](3);
        priceIds[0] = priceFeedMessages1[0].priceId;
        priceIds[1] = priceFeedMessages1[1].priceId;
        priceIds[2] = priceFeedMessages2[0].priceId;
        // parse price feeds before updating since parsing price feeds should be independent
        // of whatever is currently stored in the contract.
        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, priceIds, 0, MAX_UINT64);

        PriceFeedMessage[]
            memory expectedPriceFeedMessages = new PriceFeedMessage[](3);
        // Only the first occurrence of a valid priceFeedMessage for a paritcular priceFeed.id
        // within an updateData will be parsed which is why we exclude priceFeedMessages2[1]
        // since it has the same priceFeed.id as priceFeedMessages1[0] even though it has a later publishTime.
        // This is different than how updatePriceFeed behaves which will always update using the data
        // of the priceFeedMessage with the latest publishTime for a particular priceFeed.id
        expectedPriceFeedMessages[0] = priceFeedMessages1[0];
        expectedPriceFeedMessages[1] = priceFeedMessages1[1];
        expectedPriceFeedMessages[2] = priceFeedMessages2[0];
        for (uint i = 0; i < expectedPriceFeedMessages.length; i++) {
            assertParsedPriceFeedEqualsMessage(
                priceFeeds[i],
                expectedPriceFeedMessages[i],
                priceIds[i]
            );
        }

        // parse updateData[1] for priceFeedMessages1[0].priceId since this has the latest publishTime
        // for that priceId and should be the one that is stored.
        bytes32[] memory priceIds1 = new bytes32[](1);
        priceIds1[0] = priceFeedMessages1[0].priceId;
        bytes[] memory parseUpdateDataInput1 = new bytes[](1);
        parseUpdateDataInput1[0] = updateData[1];

        PythStructs.PriceFeed[] memory priceFeeds1 = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(parseUpdateDataInput1, priceIds1, 0, MAX_UINT64);

        pyth.updatePriceFeeds{value: updateFee}(updateData);

        // check stored price feed information matches updateData
        assertPriceFeedMessageStored(priceFeedMessages1[1]);
        assertPriceFeedMessageStored(priceFeedMessages2[0]);
        assertPriceFeedMessageStored(priceFeedMessages2[1]);

        PythStructs.PriceFeed[]
            memory expectedPriceFeeds = new PythStructs.PriceFeed[](3);
        expectedPriceFeeds[0] = priceFeeds1[0];
        expectedPriceFeeds[1] = priceFeeds[1];
        expectedPriceFeeds[2] = priceFeeds[2];

        // check stored price feed information matches parsed price feeds
        for (uint i = 0; i < expectedPriceFeeds.length; i++) {
            assertParsedPriceFeedStored(expectedPriceFeeds[i]);
        }
    }

    function testUpdatePriceFeedWithWormholeMerkleIgnoresOutOfOrderUpdateSingleCall()
        public
    {
        PriceFeedMessage[]
            memory priceFeedMessages1 = generateRandomPriceFeedMessage(1);
        PriceFeedMessage[]
            memory priceFeedMessages2 = generateRandomPriceFeedMessage(1);

        // Make the price ids the same
        priceFeedMessages2[0].priceId = priceFeedMessages1[0].priceId;
        // Adjust the timestamps so the second timestamp is smaller than the first
        // so it doesn't get stored.
        priceFeedMessages1[0].publishTime = 10;
        priceFeedMessages2[0].publishTime = 5;

        bytes[] memory updateData = new bytes[](2);

        uint8 depth = 0; // 1 messages
        uint8 numSigners = 1;

        updateData[0] = generateWhMerkleUpdate(
            priceFeedMessages1,
            depth,
            numSigners
        );
        updateData[1] = generateWhMerkleUpdate(
            priceFeedMessages2,
            depth,
            numSigners
        );

        uint updateFee = pyth.getUpdateFee(updateData);

        pyth.updatePriceFeeds{value: updateFee}(updateData);

        assertPriceFeedMessageStored(priceFeedMessages1[0]);

        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessages1[0].priceId;

        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, priceIds, 0, MAX_UINT64);
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedStored(priceFeeds[0]);

        // parsePriceFeedUpdates should return the first priceFeed in the case
        // that the updateData contains multiple feeds with the same id.
        // Swap the order of updates in updateData to verify that the other priceFeed is returned
        bytes[] memory updateData1 = new bytes[](2);
        updateData1[0] = updateData[1];
        updateData1[1] = updateData[0];

        PythStructs.PriceFeed[] memory priceFeeds1 = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData1, priceIds, 0, MAX_UINT64);
        assertEq(priceFeeds1.length, 1);
        assertEq(priceFeeds1[0].price.publishTime, 5);
    }

    function testUpdatePriceFeedWithWormholeMerkleIgnoresOutOfOrderUpdateMultiCall()
        public
    {
        PriceFeedMessage[]
            memory priceFeedMessages1 = generateRandomPriceFeedMessage(1);
        PriceFeedMessage[]
            memory priceFeedMessages2 = generateRandomPriceFeedMessage(1);

        // Make the price ids the same
        priceFeedMessages2[0].priceId = priceFeedMessages1[0].priceId;
        // Adjust the timestamps so the second timestamp is smaller than the first
        // so it doesn't get stored.
        priceFeedMessages1[0].publishTime = 10;
        priceFeedMessages2[0].publishTime = 5;

        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages1);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        (updateData, updateFee) = createWormholeMerkleUpdateData(
            priceFeedMessages2
        );
        pyth.updatePriceFeeds{value: updateFee}(updateData);
        // Make sure that the old value is still stored
        assertPriceFeedMessageStored(priceFeedMessages1[0]);
    }

    function testParsePriceFeedUpdatesWithWormholeMerkleWorksWithOutOfOrderUpdateMultiCall()
        public
    {
        PriceFeedMessage[]
            memory priceFeedMessages1 = generateRandomPriceFeedMessage(1);
        PriceFeedMessage[]
            memory priceFeedMessages2 = generateRandomPriceFeedMessage(1);

        // Make the price ids the same
        priceFeedMessages2[0].priceId = priceFeedMessages1[0].priceId;
        // Adjust the timestamps so the second timestamp is smaller than the first
        // Parse should work regardless of what's stored on chain.
        priceFeedMessages1[0].publishTime = 10;
        priceFeedMessages2[0].publishTime = 5;

        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages1);
        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessages1[0].priceId;
        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, priceIds, 0, MAX_UINT64);

        // Parse should always return the same value regardless of what's stored on chain.
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages1[0],
            priceIds[0]
        );
        pyth.updatePriceFeeds{value: updateFee}(updateData);
        priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages1[0],
            priceIds[0]
        );

        (
            bytes[] memory updateData1,
            uint updateFee1
        ) = createWormholeMerkleUpdateData(priceFeedMessages2);
        pyth.updatePriceFeeds{value: updateFee1}(updateData1);
        // reparse the original updateData should still return the same thing
        priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages1[0],
            priceIds[0]
        );

        // parsing the second message should return the data based on the second messagef
        priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee1}(
            updateData1,
            priceIds,
            0,
            MAX_UINT64
        );
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages2[0],
            priceIds[0]
        );
    }

    /// @notice This method creates a forged invalid wormhole update data.
    /// The caller should pass the forgeItem as string and if it matches the
    /// expected value, that item will be forged to be invalid.
    function createAndForgeWormholeMerkleUpdateData(
        bytes memory forgeItem
    )
        public
        returns (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        )
    {
        uint numPriceFeeds = 10;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }

        bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
            priceFeedMessages
        );

        (bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
            .constructProofs(encodedPriceFeedMessages, 4); // 4 is the depth of the tree (enough for 16 messages)

        bytes memory wormholePayload;
        unchecked {
            wormholePayload = abi.encodePacked(
                isNotMatch(forgeItem, "whMagic")
                    ? uint32(0x41555756)
                    : uint32(0x41555750),
                isNotMatch(forgeItem, "whUpdateType")
                    ? uint8(PythAccumulator.UpdateType.WormholeMerkle)
                    : uint8(PythAccumulator.UpdateType.WormholeMerkle) + 1,
                uint64(0), // Slot, not used in target networks
                uint32(0), // Storage index, not used in target networks
                isNotMatch(forgeItem, "rootDigest")
                    ? rootDigest
                    : bytes20(uint160(rootDigest) + 1)
            );
        }

        bytes memory wormholeMerkleVaa = generateVaa(
            0,
            isNotMatch(forgeItem, "whSourceChain")
                ? SOURCE_EMITTER_CHAIN_ID
                : SOURCE_EMITTER_CHAIN_ID + 1,
            isNotMatch(forgeItem, "whSourceAddress")
                ? SOURCE_EMITTER_ADDRESS
                : bytes32(
                    0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa00
                ),
            0,
            wormholePayload,
            1 // num signers
        );

        updateData = new bytes[](1);

        updateData[0] = abi.encodePacked(
            isNotMatch(forgeItem, "headerMagic")
                ? uint32(0x504e4155)
                : uint32(0x504e4150), // PythAccumulator.ACCUMULATOR_MAGIC
            isNotMatch(forgeItem, "headerMajorVersion") ? uint8(1) : uint8(2), // major version
            uint8(0), // minor version
            uint8(0), // trailing header size
            uint8(PythAccumulator.UpdateType.WormholeMerkle),
            uint16(wormholeMerkleVaa.length),
            wormholeMerkleVaa,
            uint8(priceFeedMessages.length)
        );

        for (uint i = 0; i < priceFeedMessages.length; i++) {
            updateData[0] = abi.encodePacked(
                updateData[0],
                uint16(encodedPriceFeedMessages[i].length),
                encodedPriceFeedMessages[i],
                isNotMatch(forgeItem, "proofItem") ? proofs[i] : proofs[0]
            );
        }

        // manually calculate the fee
        // so this helper method doesn't trigger the error.
        // updateFee = pyth.getUpdateFee(numPriceFeeds);
        updateFee = singleUpdateFeeInWei() * numPriceFeeds;
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongVAAPayloadMagic()
        public
    {
        // In this test the Wormhole accumulator magic is wrong and the update gets reverted.
        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("whMagic");

        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert(PythErrors.InvalidUpdateData.selector);

        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleWorksWithoutForging() public {
        // In this test we make sure the structure returned by createAndForgeWormholeMerkleUpdateData
        // is valid if no particular forge flag is set
        (
            bytes[] memory updateData,
            uint updateFee,

        ) = createAndForgeWormholeMerkleUpdateData("");

        pyth.updatePriceFeeds{value: updateFee}(updateData);
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongVAAPayloadUpdateType()
        public
    {
        // In this test the Wormhole accumulator magic is wrong and the update gets
        // reverted.

        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("whUpdateType");
        vm.expectRevert(); // Reason: Conversion into non-existent enum type. However it
        // was not possible to check the revert reason in the test.
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert();
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongVAASource()
        public
    {
        // In this test the Wormhole message source is wrong.
        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("whSourceAddress");
        vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);
        vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );

        (
            updateData,
            updateFee,
            priceIds
        ) = createAndForgeWormholeMerkleUpdateData("whSourceChain");
        vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongRootDigest()
        public
    {
        // In this test the Wormhole merkle proof digest is wrong
        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("rootDigest");
        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongProofItem()
        public
    {
        // In this test all Wormhole merkle proof items are the first item proof
        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("proofItem");
        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsOnWrongHeader()
        public
    {
        // In this test the message headers are wrong
        (
            bytes[] memory updateData,
            uint updateFee,
            bytes32[] memory priceIds
        ) = createAndForgeWormholeMerkleUpdateData("headerMagic");
        vm.expectRevert(); // The revert reason is not deterministic because when it doesn't match it goes through
        // the old approach.
        pyth.updatePriceFeeds{value: updateFee}(updateData);
        vm.expectRevert();
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );

        (
            updateData,
            updateFee,
            priceIds
        ) = createAndForgeWormholeMerkleUpdateData("headerMajorVersion");
        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.updatePriceFeeds{value: updateFee}(updateData);

        vm.expectRevert(PythErrors.InvalidUpdateData.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testUpdatePriceFeedWithWormholeMerkleRevertsIfUpdateFeeIsNotPaid()
        public
    {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        (bytes[] memory updateData, ) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }
        vm.expectRevert(PythErrors.InsufficientFee.selector);
        pyth.updatePriceFeeds{value: 0}(updateData);

        vm.expectRevert(PythErrors.InsufficientFee.selector);
        pyth.parsePriceFeedUpdates{value: 0}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testParsePriceFeedWithWormholeMerkleWorks(uint seed) public {
        setRandSeed(seed);

        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);
        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }
        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, priceIds, 0, MAX_UINT64);

        for (uint i = 0; i < priceFeeds.length; i++) {
            assertParsedPriceFeedEqualsMessage(
                priceFeeds[i],
                priceFeedMessages[i],
                priceIds[i]
            );
        }

        // update priceFeedMessages
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i].price = getRandInt64();
            priceFeedMessages[i].conf = getRandUint64();
            priceFeedMessages[i].expo = getRandInt32();
            priceFeedMessages[i].publishTime = getRandUint64();
            priceFeedMessages[i].emaPrice = getRandInt64();
            priceFeedMessages[i].emaConf = getRandUint64();
        }

        (updateData, updateFee) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        // reparse
        priceFeeds = pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );

        for (uint i = 0; i < priceFeeds.length; i++) {
            assertParsedPriceFeedEqualsMessage(
                priceFeeds[i],
                priceFeedMessages[i],
                priceIds[i]
            );
        }
    }

    function testParsePriceFeedUniqueWithWormholeMerkleWorks(uint seed) public {
        setRandSeed(seed);

        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        uint64 publishTime = getRandUint64();
        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessages[0].priceId;
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
            priceFeedMessages[i].publishTime = publishTime;
            priceFeedMessages[i].prevPublishTime = publishTime;
        }
        uint firstUpdate = (getRandUint() % numPriceFeeds);
        priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);

        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        PythStructs.PriceFeed[] memory priceFeeds = pyth
            .parsePriceFeedUpdatesUnique{value: updateFee}(
            updateData,
            priceIds,
            publishTime - 1,
            MAX_UINT64
        );

        priceFeeds = pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
            updateData,
            priceIds,
            publishTime,
            MAX_UINT64
        );
        assertEq(priceFeeds.length, 1);

        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages[firstUpdate],
            priceIds[0]
        );
    }

    function testParsePriceFeedUniqueWithWormholeMerkleUpdatesLatestPriceIfNecessary(
        uint seed
    ) public {
        setRandSeed(seed);

        uint64 numPriceFeeds = (getRandUint64() % 10) + 2;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        uint64 publishTime = getRandUint64();
        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessages[0].priceId;
        for (uint64 i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i].priceId = priceFeedMessages[0].priceId;
            priceFeedMessages[i].publishTime = publishTime;
            priceFeedMessages[i].prevPublishTime = publishTime;
        }

        // firstUpdate is the one we expect to be returned and latestUpdate is the one we expect to be stored
        uint latestUpdate = (getRandUint() % numPriceFeeds);
        priceFeedMessages[latestUpdate].prevPublishTime = publishTime + 1000;
        priceFeedMessages[latestUpdate].publishTime = publishTime + 1000;

        uint firstUpdate = (getRandUint() % numPriceFeeds);
        while (firstUpdate == latestUpdate) {
            firstUpdate = (getRandUint() % numPriceFeeds);
        }
        priceFeedMessages[firstUpdate].prevPublishTime = publishTime - 1;
        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);

        // firstUpdate is returned but latestUpdate is stored
        PythStructs.PriceFeed[] memory priceFeeds = pyth
            .parsePriceFeedUpdatesUnique{value: updateFee}(
            updateData,
            priceIds,
            publishTime,
            MAX_UINT64
        );
        assertEq(priceFeeds.length, 1);
        assertParsedPriceFeedEqualsMessage(
            priceFeeds[0],
            priceFeedMessages[firstUpdate],
            priceIds[0]
        );
        assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);

        // increase the latestUpdate publish time and make a new updateData
        priceFeedMessages[latestUpdate].publishTime = publishTime + 2000;
        (updateData, updateFee) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        // since there is a revert, the latestUpdate is not stored
        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
            updateData,
            priceIds,
            publishTime - 1,
            MAX_UINT64
        );
        assertEq(
            pyth.getPriceUnsafe(priceIds[0]).publishTime,
            publishTime + 1000
        );

        // there is no revert, the latestPrice is updated with the latestUpdate
        pyth.parsePriceFeedUpdatesUnique{value: updateFee}(
            updateData,
            priceIds,
            publishTime,
            MAX_UINT64
        );
        assertPriceFeedMessageStored(priceFeedMessages[latestUpdate]);
    }

    function testParsePriceFeedWithWormholeMerkleWorksRandomDistinctUpdatesInput(
        uint seed
    ) public {
        setRandSeed(seed);

        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );

        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);
        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }

        // Shuffle the priceFeedMessages
        for (uint i = 1; i < numPriceFeeds; i++) {
            uint swapWith = getRandUint() % (i + 1);
            (priceFeedMessages[i], priceFeedMessages[swapWith]) = (
                priceFeedMessages[swapWith],
                priceFeedMessages[i]
            );
            (priceIds[i], priceIds[swapWith]) = (
                priceIds[swapWith],
                priceIds[i]
            );
        }

        // Select only first numSelectedPriceFeeds. numSelectedPriceFeeds will be in [0, numPriceFeeds]
        uint numSelectedPriceFeeds = getRandUint() % (numPriceFeeds + 1);

        PriceFeedMessage[]
            memory selectedPriceFeedsMessages = new PriceFeedMessage[](
                numSelectedPriceFeeds
            );
        bytes32[] memory selectedPriceIds = new bytes32[](
            numSelectedPriceFeeds
        );

        for (uint i = 0; i < numSelectedPriceFeeds; i++) {
            selectedPriceFeedsMessages[i] = priceFeedMessages[i];
            selectedPriceIds[i] = priceIds[i];
        }

        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, selectedPriceIds, 0, MAX_UINT64);
        for (uint i = 0; i < numSelectedPriceFeeds; i++) {
            assertParsedPriceFeedEqualsMessage(
                priceFeeds[i],
                selectedPriceFeedsMessages[i],
                selectedPriceIds[i]
            );
        }
    }

    function testParsePriceFeedWithWormholeMerkleRevertsIfPriceIdNotIncluded()
        public
    {
        PriceFeedMessage[] memory priceFeedMessages = new PriceFeedMessage[](1);
        priceFeedMessages[0] = PriceFeedMessage({
            priceId: bytes32(uint(1)),
            price: getRandInt64(),
            conf: getRandUint64(),
            expo: getRandInt32(),
            publishTime: getRandUint64(),
            prevPublishTime: getRandUint64(),
            emaPrice: getRandInt64(),
            emaConf: getRandUint64()
        });

        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);
        bytes32[] memory priceIds = new bytes32[](1);
        priceIds[0] = bytes32(uint(2));

        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            MAX_UINT64
        );
    }

    function testParsePriceFeedUpdateRevertsIfPricesOutOfTimeRange() public {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceFeedMessages[i].publishTime = uint64(
                100 + (getRandUint() % 101)
            ); // All between [100, 200]
        }

        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);

        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }

        // Request for parse within the given time range should work
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            100,
            200
        );

        // Request for parse after the time range should revert.
        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            300,
            MAX_UINT64
        );

        // Request for parse before the time range should revert.
        vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector);
        pyth.parsePriceFeedUpdates{value: updateFee}(
            updateData,
            priceIds,
            0,
            99
        );
    }

    function testGetUpdateFeeWorksForWhMerkle() public {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        (bytes[] memory updateData, ) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        uint updateFee = pyth.getUpdateFee(updateData);
        assertEq(updateFee, SINGLE_UPDATE_FEE_IN_WEI * numPriceFeeds);
    }

    function testGetUpdateFeeWorksForWhMerkleBasedOnNumUpdates() public {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        // Set the priceId of the second message to be the same as the first.
        priceFeedMessages[1].priceId = priceFeedMessages[0].priceId;
        (bytes[] memory updateData, ) = createWormholeMerkleUpdateData(
            priceFeedMessages
        );

        uint updateFee = pyth.getUpdateFee(updateData);
        // updateFee should still be based on numUpdates not distinct number of priceIds
        assertEq(updateFee, SINGLE_UPDATE_FEE_IN_WEI * numPriceFeeds);
    }

    function testParsePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
        public
    {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        (
            bytes[] memory updateData,
            uint updateFee
        ) = createWormholeMerkleUpdateData(priceFeedMessages);

        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
        for (uint i = 0; i < numPriceFeeds; i++) {
            priceIds[i] = priceFeedMessages[i].priceId;
        }
        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
            value: updateFee
        }(updateData, priceIds, 0, MAX_UINT64);
        uint8 futureMinorVersion = uint8(2);
        uint8 futureTrailingHeaderSize = uint8(20);
        (
            bytes[] memory updateDataFromFuture,
            uint updateFeeFromFuture
        ) = createFowardCompatibleWormholeMerkleUpdateData(
                priceFeedMessages,
                futureMinorVersion,
                futureTrailingHeaderSize
            );

        PythStructs.PriceFeed[] memory priceFeedsFromFutureUpdateData = pyth
            .parsePriceFeedUpdates{value: updateFeeFromFuture}(
            updateDataFromFuture,
            priceIds,
            0,
            MAX_UINT64
        );
        assertEq(updateFee, updateFeeFromFuture);

        for (uint i = 0; i < priceFeeds.length; i++) {
            assertPriceFeedEqual(
                priceFeeds[i],
                priceFeedsFromFutureUpdateData[i]
            );
        }
    }

    function testUpdatePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
        public
    {
        uint numPriceFeeds = (getRandUint() % 10) + 1;
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomPriceFeedMessage(
                numPriceFeeds
            );
        uint8 futureMinorVersion = uint8(2);
        uint8 futureTrailingHeaderSize = uint8(20);
        (
            bytes[] memory forwardCompatibleUpdateData,
            uint updateFee
        ) = createFowardCompatibleWormholeMerkleUpdateData(
                priceFeedMessages,
                futureMinorVersion,
                futureTrailingHeaderSize
            );

        pyth.updatePriceFeeds{value: updateFee}(forwardCompatibleUpdateData);

        for (uint i = 0; i < priceFeedMessages.length; i++) {
            assertPriceFeedMessageStored(priceFeedMessages[i]);
        }
    }
}
