// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
import "src/interfaces/ISwapAdapterTypes.sol";
import "src/libraries/FractionMath.sol";
import "src/libraries/EfficientERC20.sol";

contract AdapterTest is Test, ISwapAdapterTypes {
    using FractionMath for Fraction;
    using EfficientERC20 for IERC20;

    string[] public stringPctgs = ["0%", "0.1%", "50%", "100%"];

    // @notice Test the behavior of a swap adapter for a list of pools
    // @dev Computes limits, prices, and swaps on the pools on both directions
    // for different
    // sell amounts. Asserts that the prices behaves as expected.
    // @param adapter The swap adapter to test
    // @param poolIds The list of pool ids to test
    function runPoolBehaviourTest(
        ISwapAdapter adapter,
        bytes32[] memory poolIds
    ) public {
        bool hasPriceImpact = !hasCapability(
            adapter.getCapabilities(poolIds[0], address(0), address(0)),
            Capability.ConstantPrice
        );
        for (uint256 i = 0; i < poolIds.length; i++) {
            address[] memory tokens = adapter.getTokens(poolIds[i]);
            IERC20(tokens[0]).forceApprove(address(adapter), type(uint256).max);
            IERC20(tokens[1]).forceApprove(address(adapter), type(uint256).max);

            testPricesForPair(
                adapter, poolIds[i], tokens[0], tokens[1], hasPriceImpact
            );
            testPricesForPair(
                adapter, poolIds[i], tokens[1], tokens[0], hasPriceImpact
            );
        }
    }

    // Prices should:
    // 1. Be monotonic decreasing (within rounding tolerance)
    // 2. Be positive
    // 3. Always be >= the executed price and >= the price after the swap
    // (within rounding tolerance)
    function testPricesForPair(
        ISwapAdapter adapter,
        bytes32 poolId,
        address tokenIn,
        address tokenOut,
        bool hasPriceImpact
    ) internal {
        uint256 sellLimit = adapter.getLimits(poolId, tokenIn, tokenOut)[0];
        assertGt(sellLimit, 0, "Sell limit should be greater than 0");

        console2.log(
            "TEST: Testing prices for pair %s -> %s. Sell limit: %d",
            tokenIn,
            tokenOut,
            sellLimit
        );

        bool hasMarginalPrices = hasCapability(
            adapter.getCapabilities(poolId, tokenIn, tokenOut),
            Capability.MarginalPrice
        );
        uint256[] memory amounts =
            calculateTestAmounts(sellLimit, hasMarginalPrices);

        // TODO: What if the price function is not available? Do we still want
        // to run this test?
        Fraction[] memory prices =
            adapter.price(poolId, tokenIn, tokenOut, amounts);
        assertGt(
            fractionToInt(prices[0])
                // within rounding tolerance
                * (amounts[amounts.length - 1] + 1)
                / amounts[amounts.length - 1],
            fractionToInt(prices[prices.length - 1]),
            "Price at limit should be smaller than price at 0"
        );
        console2.log(
            "TEST: Price at 0: %d, price at sell limit: %d",
            fractionToInt(prices[0]),
            fractionToInt(prices[prices.length - 1])
        );

        console2.log("TEST: Testing behavior for price at 0");
        assertGt(prices[0].numerator, 0, "Nominator shouldn't be 0");
        assertGt(prices[0].denominator, 0, "Denominator shouldn't be 0");
        uint256 priceAtZero = fractionToInt(prices[0]);
        console2.log("TEST: Price at 0: %d", priceAtZero);

        deal(tokenIn, address(this), 5 * amounts[amounts.length - 1]);

        uint256 initialState = vm.snapshot();

        for (uint256 j = 1; j < amounts.length; j++) {
            console2.log(
                "TEST: Testing behavior for price at %s of limit.",
                stringPctgs[j],
                amounts[j]
            );
            uint256 priceAtAmount = fractionToInt(prices[j]);
            // We allow the assertions to tolerate rounding errors
            // not greater than `1/amounts[j]`
            uint256 toleranceDenominator = amounts[j];

            console2.log("TEST: Swapping %d of %s", amounts[j], tokenIn);
            try adapter.swap(
                poolId, tokenIn, tokenOut, OrderSide.Sell, amounts[j]
            ) returns (
                Trade memory trade
            ) {
                uint256 executedPrice = Fraction(
                        trade.calculatedAmount, amounts[j]
                    ).toQ128x128();
                uint256 priceAfterSwap = fractionToInt(trade.price);
                console2.log("TEST:  - Executed price:   %d", executedPrice);
                console2.log("TEST:  - Price at amount:  %d", priceAtAmount);
                console2.log("TEST:  - Price after swap: %d", priceAfterSwap);

                if (hasPriceImpact) {
                    assertGeTol(
                        executedPrice,
                        priceAtAmount,
                        toleranceDenominator,
                        "Price should be greater than executed price."
                    );
                    assertGtTol(
                        executedPrice,
                        priceAfterSwap,
                        toleranceDenominator,
                        "Executed price should be greater than price after swap."
                    );
                    assertGtTol(
                        priceAtZero,
                        executedPrice,
                        toleranceDenominator,
                        "Price should be greater than price after swap."
                    );
                } else {
                    assertGeTol(
                        priceAtZero,
                        priceAfterSwap,
                        toleranceDenominator,
                        "Executed price should be or equal to price after swap."
                    );
                    assertGeTol(
                        priceAtZero,
                        priceAtAmount,
                        toleranceDenominator,
                        "Executed price should be or equal to price after swap."
                    );
                    assertGeTol(
                        priceAtZero,
                        executedPrice,
                        toleranceDenominator,
                        "Price should be or equal to price after swap."
                    );
                }
            } catch (bytes memory reason) {
                (bool isTooSmall, uint256 lowerLimit) =
                    decodeTooSmallError(reason);
                (bool isLimitExceeded, uint256 limit) =
                    decodeLimitExceededError(reason);

                if (isTooSmall) {
                    // We allow a TooSmall exception to occur for the smallest
                    // amount only.
                    if (j == 1) {
                        console2.log(
                            "TEST: TooSmall exception tolerated for smallest amount"
                        );
                    } else {
                        revert(
                            "TEST: TooSmall thrown for a significantly sized amount"
                        );
                    }
                } else if (isLimitExceeded) {
                    // We never allow LimitExceeded to be thrown, since all
                    // amounts should be within the stated limits.
                    revert(
                        "TEST: LimitExceeded thrown for an amount within limits"
                    );
                } else {
                    // any other revert reason bubbles up
                    assembly {
                        revert(add(reason, 32), mload(reason))
                    }
                }
            }

            vm.revertTo(initialState);
        }
        uint256 amountAboveLimit = sellLimit * 105 / 100;

        bool hasHardLimits = hasCapability(
            adapter.getCapabilities(poolId, tokenIn, tokenOut),
            Capability.HardLimits
        );

        if (hasHardLimits) {
            testRevertAboveLimit(
                adapter, poolId, tokenIn, tokenOut, amountAboveLimit
            );
        } else {
            testOperationsAboveLimit(
                adapter, poolId, tokenIn, tokenOut, amountAboveLimit
            );
        }

        console2.log("TEST: All tests passed.");
    }

    function testRevertAboveLimit(
        ISwapAdapter adapter,
        bytes32 poolId,
        address tokenIn,
        address tokenOut,
        uint256 amountAboveLimit
    ) internal {
        console2.log(
            "TEST: Testing revert behavior above the sell limit: %d",
            amountAboveLimit
        );
        uint256[] memory aboveLimitArray = new uint256[](1);
        aboveLimitArray[0] = amountAboveLimit;
        bool supportsLimitExceeded = false;

        try adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray) {
            revert(
                "Pool shouldn't be able to fetch prices above the sell limit"
            );
        } catch (bytes memory reason) {
            (bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason);
            (bool isLimitExceeded, uint256 limit) =
                decodeLimitExceededError(reason);

            if (isLimitExceeded) {
                supportsLimitExceeded = true;
                console2.log(
                    "TEST: LimitExceeded supported! Thrown when fetching price above limit: %i",
                    limit
                );
            } else if (isTooSmall) {
                console2.log(
                    "TEST: UNEXPECTED TooSmall error when fetching price below limit: %i",
                    lowerLimit
                );
                revert TooSmall(lowerLimit);
            } else if (
                reason.length >= 4
                    && bytes4(reason) == bytes4(keccak256("Error(string)"))
            ) {
                string memory s = abi.decode(
                    sliceBytes(reason, 4, reason.length - 4), (string)
                );
                console2.log(
                    "TEST: Expected error when fetching price above limit: %s",
                    s
                );
            } else {
                // Unexpected error type: re-raise.
                assembly {
                    revert(add(reason, 32), mload(reason))
                }
            }
        }
        try adapter.swap(
            poolId, tokenIn, tokenOut, OrderSide.Sell, aboveLimitArray[0]
        ) {
            revert("Pool shouldn't be able to swap above the sell limit");
        } catch (bytes memory reason) {
            (bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason);
            (bool isLimitExceeded, uint256 limit) =
                decodeLimitExceededError(reason);

            if (isLimitExceeded) {
                supportsLimitExceeded = true;
                console2.log(
                    "TEST: LimitExceeded supported! Thrown when swapping above limit: %i",
                    limit
                );
            } else if (isTooSmall) {
                console2.log(
                    "TEST: UNEXPECTED TooSmall error when swapping above limit: %i",
                    lowerLimit
                );
                revert TooSmall(lowerLimit);
            } else if (
                reason.length >= 4
                    && bytes4(reason) == bytes4(keccak256("Error(string)"))
            ) {
                string memory s = abi.decode(
                    sliceBytes(reason, 4, reason.length - 4), (string)
                );
                console2.log(
                    "TEST: Expected error when swapping above limit: %s", s
                );
            } else {
                // Unexpected error type: re-raise.
                assembly {
                    revert(add(reason, 32), mload(reason))
                }
            }
        }
        if (supportsLimitExceeded) {
            console.log(unicode"Adapter supports LimitExceeded ✓");
        }
    }

    function testOperationsAboveLimit(
        ISwapAdapter adapter,
        bytes32 poolId,
        address tokenIn,
        address tokenOut,
        uint256 amountAboveLimit
    ) internal {
        console2.log(
            "TEST: Testing operations above the sell limit: %d",
            amountAboveLimit
        );
        uint256[] memory aboveLimitArray = new uint256[](1);
        aboveLimitArray[0] = amountAboveLimit;

        adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray);
        adapter.swap(
            poolId, tokenIn, tokenOut, OrderSide.Sell, aboveLimitArray[0]
        );
    }

    function calculateTestAmounts(uint256 limit, bool hasMarginalPrices)
        internal
        pure
        returns (uint256[] memory)
    {
        uint256[] memory amounts = new uint256[](4);
        amounts[0] = hasMarginalPrices ? 0 : limit / 10000;
        amounts[1] = limit / 1000;
        amounts[2] = limit / 2;
        amounts[3] = limit;
        return amounts;
    }

    function fractionToInt(Fraction memory price)
        public
        pure
        returns (uint256)
    {
        return price.toQ128x128();
    }

    function hasCapability(
        Capability[] memory capabilities,
        Capability capability
    ) internal pure returns (bool) {
        for (uint256 i = 0; i < capabilities.length; i++) {
            if (capabilities[i] == capability) {
                return true;
            }
        }

        return false;
    }

    //
    // Custom Error Helper Functions
    // TODO should we expose these in a better location / library for solvers to
    // also leverage?

    // Helper function to check if error is TooSmall and decode it
    function decodeTooSmallError(bytes memory reason)
        internal
        pure
        returns (bool, uint256)
    {
        if (reason.length >= 4 && bytes4(reason) == TooSmall.selector) {
            if (reason.length == 36) {
                uint256 lowerLimit =
                    abi.decode(sliceBytes(reason, 4, 32), (uint256));
                return (true, lowerLimit);
            }
        }
        return (false, 0);
    }

    // Helper function to check if error is LimitExceeded and decode it
    function decodeLimitExceededError(bytes memory reason)
        internal
        pure
        returns (bool, uint256)
    {
        if (reason.length >= 4 && bytes4(reason) == LimitExceeded.selector) {
            if (reason.length == 36) {
                uint256 limit = abi.decode(sliceBytes(reason, 4, 32), (uint256));
                return (true, limit);
            }
        }
        return (false, 0);
    }

    // Helper function to slice bytes
    function sliceBytes(bytes memory data, uint256 start, uint256 length)
        internal
        pure
        returns (bytes memory)
    {
        bytes memory result = new bytes(length);
        for (uint256 i = 0; i < length; i++) {
            result[i] = data[start + i];
        }
        return result;
    }

    //
    // Helper functions to assert with tolerance
    //

    function assertGeTol(
        uint256 a,
        uint256 b,
        uint256 toleranceDenominator,
        string memory errorMessage
    ) internal {
        // The tolerance is `1 / toleranceDenominator`, so we increase the value
        // of `a` by this amount. adjustedA = a * (denom+1) / denom
        uint256 adjustedA = FractionMath.mulDiv(
            a, toleranceDenominator + 1, toleranceDenominator
        );
        assertGe(adjustedA, b, errorMessage);
    }

    function assertGtTol(
        uint256 a,
        uint256 b,
        uint256 toleranceDenominator,
        string memory errorMessage
    ) internal {
        // The tolerance is `1 / toleranceDenominator`, so we increase the value
        // of `a` by this amount. adjustedA = a * (denom+1) / denom
        uint256 adjustedA = FractionMath.mulDiv(
            a, toleranceDenominator + 1, toleranceDenominator
        );
        assertGt(adjustedA, b, errorMessage);
    }
}
