// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;

import "forge-std/console2.sol"; // todo

import {
    IERC20
} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {
    IERC20Metadata
} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {FractionMath} from "../src/libraries/FractionMath.sol";
import {IPartyInfo} from "../src/liquidityparty/IPartyInfo.sol";
import {IPartyPlanner} from "../src/liquidityparty/IPartyPlanner.sol";
import {IPartyPool} from "../src/liquidityparty/IPartyPool.sol";
import {
    LiquidityPartySwapAdapter
} from "../src/liquidityparty/LiquidityPartySwapAdapter.sol";
import {AdapterTest} from "./AdapterTest.sol";

contract LiquidityPartyFunctionTest is AdapterTest {
    using FractionMath for Fraction;

    IPartyPlanner internal constant PLANNER =
        IPartyPlanner(0x42977f565971F6D288a05ddEbC87A17276F71A29);
    IPartyInfo internal constant INFO =
        IPartyInfo(0x605F803cD27F5c1fa01440B2cbd5D3E4Cf7EE850);
    address internal constant MINT_IMPL =
        0xA0375403921e9B357E1BeD57bef3fA3FCE80acd0;
    address internal constant SWAP_IMPL =
        0x6aA001e87F86E83bc4D569883332882cb47E2A13;
    IPartyPool internal constant POOL =
        IPartyPool(0x2A804e94500AE379ee0CcC423a67B07cc0aF548C);
    bytes32 internal constant POOL_ID = bytes32(bytes20(address(POOL)));
    uint256 internal constant FORK_BLOCK = 23978797; // block in which the pool
    // was created

    LiquidityPartySwapAdapter internal adapter;
    uint256 internal constant TEST_ITERATIONS = 10;

    address[] internal tokens;
    address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address internal constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
    address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address internal constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
    address internal constant WSOL = 0xD31a59c85aE9D8edEFeC411D448f90841571b89c;
    address internal constant TRX = 0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5;
    address internal constant AAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9;
    address internal constant PEPE = 0x6982508145454Ce325dDbE47a25d4ec3d2311933;
    address internal constant SHIB = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE;

    address private constant INPUT_TOKEN = WBTC;
    uint8 private constant INPUT_INDEX = 2;
    address private constant OUTPUT_TOKEN = SHIB;
    uint8 private constant OUTPUT_INDEX = 9;

    function setUp() public {
        tokens = new address[](10);
        tokens[0] = USDT;
        tokens[1] = USDC;
        tokens[2] = WBTC;
        tokens[3] = WETH;
        tokens[4] = UNI;
        tokens[5] = WSOL;
        tokens[6] = TRX;
        tokens[7] = AAVE;
        tokens[8] = PEPE;
        tokens[9] = SHIB;

        vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK);

        adapter = new LiquidityPartySwapAdapter(PLANNER, INFO);

        vm.label(address(PLANNER), "PartyPlanner");
        vm.label(address(INFO), "PartyInfo");
        vm.label(address(MINT_IMPL), "PartyPoolMintImpl");
        vm.label(address(SWAP_IMPL), "PartyPoolSwapImpl");
        vm.label(address(POOL), "PartyPool");
        vm.label(address(adapter), "LiquidityPartySwapAdapter");
        for (uint256 i = 0; i < tokens.length; i++) {
            vm.label(address(tokens[i]), IERC20Metadata(tokens[i]).symbol());
        }
    }

    function testPrice() public view {
        uint256[] memory amounts = new uint256[](3);
        uint256 balance = IERC20(INPUT_TOKEN).balanceOf(address(POOL));
        amounts[0] = 2; // cannot use 1: the fee will round up and take
        // everything, resulting in a zero-output reversion
        amounts[1] = balance;
        amounts[2] = balance * 2;

        Fraction[] memory prices =
            adapter.price(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, amounts);

        for (uint256 i = 0; i < prices.length; i++) {
            assertGt(prices[i].numerator, 0);
            assertGt(prices[i].denominator, 0);
        }
    }

    function testPriceDecreasing() public view {
        uint256[] memory limits =
            adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN);

        uint256[] memory amounts = new uint256[](TEST_ITERATIONS);

        for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
            // The first entry will be a zero amount which returns the current
            // marginal price.
            amounts[i] = limits[0] * i / (TEST_ITERATIONS - 1);
        }

        Fraction[] memory prices =
            adapter.price(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, amounts);

        for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
            console2.log("compare price", prices[i].numerator);
            console2.log("             ", prices[i].denominator);
            console2.log("           > ", prices[i + 1].numerator);
            console2.log("             ", prices[i + 1].denominator);
            console2.log();
            assertEq(prices[i].compareFractions(prices[i + 1]), 1);
        }
    }

    function testSwapFuzz(uint256 amount) public {
        uint256[] memory limits =
            adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN);
        vm.assume(amount > 1); // 1 will not work because we take fee-on-input
        // and round up, leaving nothing to trade
        vm.assume(amount <= limits[0]);

        deal(INPUT_TOKEN, address(this), amount);
        IERC20(INPUT_TOKEN).approve(address(adapter), amount);

        uint256 usdtBalance = IERC20(INPUT_TOKEN).balanceOf(address(this));
        uint256 wethBalance = IERC20(OUTPUT_TOKEN).balanceOf(address(this));

        Trade memory trade = adapter.swap(
            POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, OrderSide.Sell, amount
        );

        if (trade.calculatedAmount > 0) {
            assertEq(
                amount,
                usdtBalance - IERC20(INPUT_TOKEN).balanceOf(address(this))
            );
            assertEq(
                trade.calculatedAmount,
                IERC20(OUTPUT_TOKEN).balanceOf(address(this)) - wethBalance
            );
        }
    }

    function testSwapSellIncreasing() public {
        uint256[] memory limits =
            adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN);
        uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
        Trade[] memory trades = new Trade[](TEST_ITERATIONS);

        for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
            amounts[i] = limits[0] * (i + 1) / (TEST_ITERATIONS - 1);

            uint256 beforeSwap = vm.snapshot();

            deal(INPUT_TOKEN, address(this), amounts[i]);
            IERC20(INPUT_TOKEN).approve(address(adapter), amounts[i]);
            trades[i] = adapter.swap(
                POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, OrderSide.Sell, amounts[i]
            );

            vm.revertTo(beforeSwap);
        }

        for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
            assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
            assertEq(
                trades[i].price.denominator, trades[i + 1].price.denominator
            ); // must share a basis
            assertGe(trades[i].price.numerator, trades[i + 1].price.numerator);
        }
    }

    function testGetLimits() public view {
        uint256[] memory limits =
            adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN);

        assert(limits.length == 2);
        assert(limits[0] > 0);
        assert(limits[1] > 0);
    }

    function testGetTokens() public view {
        address[] memory adapterTokens = adapter.getTokens(POOL_ID);
        for (uint256 i = 0; i < tokens.length; i++) {
            assertEq(adapterTokens[i], tokens[i]);
        }
    }

    function testGetPoolIds() public view {
        uint256 offset = 0;
        uint256 limit = 10;
        bytes32[] memory poolIds = adapter.getPoolIds(offset, limit);

        assertLe(
            poolIds.length,
            limit,
            "Number of pool IDs should be less than or equal to limit"
        );
        if (poolIds.length > 0) {
            assertGt(uint256(poolIds[0]), 0, "Pool ID should be greater than 0");
        }
    }

    // Many of the tests above seem entirely redundant with runPoolBehaviorTest
    // :shrug:
    function testLiquidityPartyPoolBehaviour() public {
        bytes32[] memory poolIds = new bytes32[](1);
        poolIds[0] = POOL_ID;
        runPoolBehaviourTest(adapter, poolIds);
    }
}
