// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.0;

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/aave/interfaces/IPriceOracleGetter.sol";
import "../contracts/aave/PythPriceOracleGetter.sol";
import "./Pyth.WormholeMerkleAccumulator.t.sol";

contract PythAaveTest is PythWormholeMerkleAccumulatorTest {
    IPriceOracleGetter public pythOracleGetter;
    address[] assets;
    bytes32[] priceIds;
    uint constant NUM_PRICE_FEEDS = 5;
    uint256 constant BASE_CURRENCY_UNIT = 1e8;
    uint constant VALID_TIME_PERIOD_SECS = 60;

    function setUp() public override {
        pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
        assets = new address[](NUM_PRICE_FEEDS);
        PriceFeedMessage[]
            memory priceFeedMessages = generateRandomBoundedPriceFeedMessage(
                NUM_PRICE_FEEDS
            );
        priceIds = new bytes32[](NUM_PRICE_FEEDS);

        for (uint i = 0; i < NUM_PRICE_FEEDS; i++) {
            assets[i] = address(
                uint160(uint(keccak256(abi.encodePacked(i + NUM_PRICE_FEEDS))))
            );
            priceIds[i] = priceFeedMessages[i].priceId;
        }

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

        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            BASE_CURRENCY_UNIT,
            VALID_TIME_PERIOD_SECS
        );
    }

    function testConversion(
        int64 pythPrice,
        int32 pythExpo,
        uint256 aavePrice,
        uint256 baseCurrencyUnit
    ) private {
        PriceFeedMessage[] memory priceFeedMessages = new PriceFeedMessage[](1);
        PriceFeedMessage memory priceFeedMessage = PriceFeedMessage({
            priceId: getRandBytes32(),
            price: pythPrice,
            conf: getRandUint64(),
            expo: pythExpo,
            publishTime: uint64(1),
            prevPublishTime: getRandUint64(),
            emaPrice: getRandInt64(),
            emaConf: getRandUint64()
        });
        priceFeedMessages[0] = priceFeedMessage;

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

        priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessage.priceId;
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            baseCurrencyUnit,
            VALID_TIME_PERIOD_SECS
        );

        assertEq(pythOracleGetter.getAssetPrice(assets[0]), aavePrice);
    }

    function testGetAssetPriceWorks() public {
        // "display" price is 529.30903
        testConversion(52_930_903, -5, 52_930_903_000, BASE_CURRENCY_UNIT);
    }

    function testGetAssetPriceWorksWithPositiveExponent() public {
        // "display" price is 5_293_000
        testConversion(5_293, 3, 529_300_000_000_000, BASE_CURRENCY_UNIT);
    }

    function testGetAssetPriceWorksWithZeroExponent() public {
        // "display" price is 5_293
        testConversion(5_293, 0, 529_300_000_000, BASE_CURRENCY_UNIT);
    }

    function testGetAssetPriceWorksWithNegativeNormalizerExponent() public {
        // "display" price is 5_293
        testConversion(
            5_293_000_000_000_000,
            -12,
            529_300_000_000,
            BASE_CURRENCY_UNIT
        );
    }

    function testGetAssetPriceWorksWithBaseCurrencyUnitOfOne() public {
        // "display" price is 529.30903
        testConversion(52_930_903, -5, 529, 1);
    }

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

        for (uint i = 0; i < assets.length; i++) {
            address asset = assets[i];
            uint256 assetPrice = pythOracleGetter.getAssetPrice(asset);
            uint256 aavePrice = assetPrice / BASE_CURRENCY_UNIT;

            bytes32 priceId = priceIds[i];
            PythStructs.Price memory price = pyth.getPrice(priceId);
            int64 pythRawPrice = price.price;
            uint pythNormalizer;
            uint pythPrice;
            if (price.expo < 0) {
                pythNormalizer = 10 ** uint32(-price.expo);
                pythPrice = uint64(pythRawPrice) / pythNormalizer;
            } else {
                pythNormalizer = 10 ** uint32(price.expo);
                pythPrice = uint64(pythRawPrice) * pythNormalizer;
            }
            assertEq(aavePrice, pythPrice);
        }
    }

    function testGetAssetPriceWorksIfGivenBaseCurrencyAddress() public {
        address usdAddress = address(0x0);
        uint256 assetPrice = pythOracleGetter.getAssetPrice(usdAddress);
        assertEq(assetPrice, BASE_CURRENCY_UNIT);
    }

    function testGetAssetRevertsIfPriceNotRecentEnough() public {
        uint timestamp = block.timestamp;
        vm.warp(timestamp + VALID_TIME_PERIOD_SECS);
        for (uint i = 0; i < assets.length; i++) {
            pythOracleGetter.getAssetPrice(assets[i]);
        }
        vm.warp(timestamp + VALID_TIME_PERIOD_SECS + 1);
        for (uint i = 0; i < assets.length; i++) {
            vm.expectRevert(PythErrors.StalePrice.selector);
            pythOracleGetter.getAssetPrice(assets[i]);
        }
    }

    function testGetAssetRevertsIfPriceFeedNotFound() public {
        address addr = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );
        vm.expectRevert(PythErrors.PriceFeedNotFound.selector);
        pythOracleGetter.getAssetPrice(addr);
    }

    function testGetAssetPriceRevertsIfPriceIsNegative() public {
        PriceFeedMessage[] memory priceFeedMessages = new PriceFeedMessage[](1);
        PriceFeedMessage memory priceFeedMessage = PriceFeedMessage({
            priceId: getRandBytes32(),
            price: int64(-5),
            conf: getRandUint64(),
            expo: getRandInt32(),
            publishTime: uint64(1),
            prevPublishTime: getRandUint64(),
            emaPrice: getRandInt64(),
            emaConf: getRandUint64()
        });

        priceFeedMessages[0] = priceFeedMessage;

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

        priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessage.priceId;
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            BASE_CURRENCY_UNIT,
            VALID_TIME_PERIOD_SECS
        );

        vm.expectRevert(abi.encodeWithSignature("InvalidNonPositivePrice()"));
        pythOracleGetter.getAssetPrice(assets[0]);
    }

    function testGetAssetPriceRevertsIfNormalizerOverflows() public {
        PriceFeedMessage[] memory priceFeedMessages = new PriceFeedMessage[](1);
        PriceFeedMessage memory priceFeedMessage = PriceFeedMessage({
            priceId: getRandBytes32(),
            price: int64(1),
            conf: getRandUint64(),
            expo: int32(59), // type(uint192).max = ~6.27e58
            publishTime: uint64(1),
            prevPublishTime: getRandUint64(),
            emaPrice: getRandInt64(),
            emaConf: getRandUint64()
        });

        priceFeedMessages[0] = priceFeedMessage;

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

        priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessage.priceId;
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            BASE_CURRENCY_UNIT,
            VALID_TIME_PERIOD_SECS
        );

        vm.expectRevert(abi.encodeWithSignature("NormalizationOverflow()"));
        pythOracleGetter.getAssetPrice(assets[0]);
    }

    function testGetAssetPriceRevertsIfNormalizedToZero() public {
        PriceFeedMessage[] memory priceFeedMessages = new PriceFeedMessage[](1);
        PriceFeedMessage memory priceFeedMessage = PriceFeedMessage({
            priceId: getRandBytes32(),
            price: int64(1),
            conf: getRandUint64(),
            expo: int32(-75),
            publishTime: uint64(1),
            prevPublishTime: getRandUint64(),
            emaPrice: getRandInt64(),
            emaConf: getRandUint64()
        });

        priceFeedMessages[0] = priceFeedMessage;

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

        priceIds = new bytes32[](1);
        priceIds[0] = priceFeedMessage.priceId;
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            BASE_CURRENCY_UNIT,
            VALID_TIME_PERIOD_SECS
        );

        vm.expectRevert(abi.encodeWithSignature("InvalidNonPositivePrice()"));
        pythOracleGetter.getAssetPrice(assets[0]);
    }

    function testPythPriceOracleGetterConstructorRevertsIfAssetsAndPriceIdsLengthAreDifferent()
        public
    {
        priceIds = new bytes32[](2);
        priceIds[0] = getRandBytes32();
        priceIds[1] = getRandBytes32();
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        vm.expectRevert(abi.encodeWithSignature("InconsistentParamsLength()"));
        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            BASE_CURRENCY_UNIT,
            VALID_TIME_PERIOD_SECS
        );
    }

    function testPythPriceOracleGetterConstructorRevertsIfInvalidBaseCurrencyUnit()
        public
    {
        priceIds = new bytes32[](1);
        priceIds[0] = getRandBytes32();
        assets = new address[](1);
        assets[0] = address(
            uint160(uint(keccak256(abi.encodePacked(uint(100)))))
        );

        vm.expectRevert(abi.encodeWithSignature("InvalidBaseCurrencyUnit()"));
        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            0,
            VALID_TIME_PERIOD_SECS
        );

        vm.expectRevert(abi.encodeWithSignature("InvalidBaseCurrencyUnit()"));
        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            11,
            VALID_TIME_PERIOD_SECS
        );

        vm.expectRevert(abi.encodeWithSignature("InvalidBaseCurrencyUnit()"));
        pythOracleGetter = new PythPriceOracleGetter(
            address(pyth),
            assets,
            priceIds,
            address(0x0),
            20,
            VALID_TIME_PERIOD_SECS
        );
    }
}
