// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
/* solhint-disable erc20-unchecked-transfer */

import "forge-std/Test.sol";
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol";
import "../src/PartyPlanner.sol";
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {Deploy} from "./Deploy.sol";

/// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IERC3156FlashBorrower {
    enum Action {
        NORMAL,               // Normal repayment
        REPAY_NONE,           // Don't repay anything
        REPAY_PARTIAL,        // Repay less than required
        REPAY_NO_FEE,         // Repay only the principal without fee
        REPAY_EXACT           // Repay exactly the required amount
    }

    Action public action;
    address public pool;
    address public payer;

    constructor(address _pool) {
        pool = _pool;
    }

    function setAction(Action _action, address _payer) external {
        action = _action;
        payer = _payer;
    }

    function onFlashLoan(
        address /*initiator*/,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata /* data */
    ) external override returns (bytes32) {
        require(msg.sender == pool, "Callback not called by pool");

        if (action == Action.NORMAL) {
            // Normal repayment
            // We received 'amount' from the pool, need to pay back amount + fee
            uint256 repaymentAmount = amount + fee;

            // Transfer the fee from payer to this contract
            // (we already have the principal 'amount' from the flash loan)
            TestERC20(token).transferFrom(payer, address(this), fee);

            // Approve pool to pull back the full repayment
            TestERC20(token).approve(pool, repaymentAmount);
        } else if (action == Action.REPAY_PARTIAL) {
            // Repay half of the required amount
            uint256 partialRepayment = (amount + fee) / 2;
            TestERC20(token).approve(pool, partialRepayment);
        } else if (action == Action.REPAY_NO_FEE) {
            // Repay only the principal without fee (we already have it from the loan)
            TestERC20(token).approve(pool, amount);
        } else if (action == Action.REPAY_EXACT) {
            // Repay exactly what was required
            uint256 repaymentAmount = amount + fee;
            // Transfer the fee from payer (we have the principal from the loan)
            TestERC20(token).transferFrom(payer, address(this), fee);
            // Approve pool to pull back the full repayment
            TestERC20(token).approve(pool, repaymentAmount);
        }
        // For REPAY_NONE, do nothing (don't approve repayment)

        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}

/// @notice Minimal ERC20 token for tests with an external mint function.
contract TestERC20 is ERC20 {
    constructor(string memory name_, string memory symbol_, uint256 initialSupply) ERC20(name_, symbol_) {
        if (initialSupply > 0) {
            _mint(msg.sender, initialSupply);
        }
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }

    // Expose convenient approve helper for tests (not necessary but handy)
    function approveMax(address spender) external {
        _approve(msg.sender, spender, type(uint256).max);
    }
}

/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
contract GasTest is Test {
    using ABDKMath64x64 for int128;
    using SafeERC20 for TestERC20;

    PartyPlanner internal planner;
    PartyPool internal pool2;
    PartyPool internal pool10;
    PartyPool internal pool20;
    PartyPool internal pool50;

    address internal alice;
    address internal bob;

    // Common parameters
    int128 internal tradeFrac;
    int128 internal targetSlippage;

    uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
    uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)

    /// @notice Helper function to create a pool with the specified number of _tokens
    function createPool(uint256 numTokens) internal returns (PartyPool) {
        // Deploy _tokens dynamically
        address[] memory tokens = new address[](numTokens);
        uint256[] memory bases = new uint256[](numTokens);

        for (uint256 i = 0; i < numTokens; i++) {
            string memory name = string(abi.encodePacked("T", vm.toString(i)));
            TestERC20 token = new TestERC20(name, name, 0);
            tokens[i] = address(token);
            bases[i] = BASE;

            // Mint initial balances for pool initialization and test users
            token.mint(address(this), INIT_BAL);
            token.mint(alice, INIT_BAL);
            token.mint(bob, INIT_BAL);
        }

        // Deploy pool with a small fee to test fee-handling paths (use 1000 ppm = 0.1%)
        uint256 feePpm = 1000;
        string memory poolName = string(abi.encodePacked("LP", vm.toString(numTokens)));
        IERC20[] memory ierc20Tokens = new IERC20[](tokens.length);
        for (uint i = 0; i < tokens.length; i++) {
            ierc20Tokens[i] = IERC20(tokens[i]);
        }
        // Compute kappa from slippage params and number of _tokens, then construct pool with kappa
        int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
        PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);

        // Transfer initial deposit amounts into pool before initial mint
        for (uint256 i = 0; i < numTokens; i++) {
            TestERC20(tokens[i]).transfer(address(newPool), INIT_BAL);
        }

        // Perform initial mint (initial deposit); receiver is this contract
        newPool.initialMint(address(this), 0);

        return newPool;
    }

    /// @notice Helper to create a pool with the stable-pair optimization enabled
    function createPoolStable(uint256 numTokens) internal returns (PartyPool) {
        // Deploy _tokens dynamically
        address[] memory tokens = new address[](numTokens);
        uint256[] memory bases = new uint256[](numTokens);

        for (uint256 i = 0; i < numTokens; i++) {
            string memory name = string(abi.encodePacked("T", vm.toString(i)));
            TestERC20 token = new TestERC20(name, name, 0);
            tokens[i] = address(token);
            bases[i] = BASE;

            // Mint initial balances for pool initialization and test users
            token.mint(address(this), INIT_BAL);
            token.mint(alice, INIT_BAL);
            token.mint(bob, INIT_BAL);
        }

        // Deploy pool with a small fee to test fee-handling paths (use 1000 ppm = 0.1%)
        uint256 feePpm = 1000;
        string memory poolName = string(abi.encodePacked("LPs", vm.toString(numTokens)));
        // Note the final 'true' arg to activate stable-pair optimization path
        IERC20[] memory ierc20Tokens = new IERC20[](tokens.length);
        for (uint i = 0; i < tokens.length; i++) {
            ierc20Tokens[i] = IERC20(tokens[i]);
        }
        int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
        PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);

        // Transfer initial deposit amounts into pool before initial mint
        for (uint256 i = 0; i < numTokens; i++) {
            TestERC20(tokens[i]).transfer(address(newPool), INIT_BAL);
        }

        // Perform initial mint (initial deposit); receiver is this contract
        newPool.initialMint(address(this), 0);

        return newPool;
    }

    function setUp() public {
        alice = address(0xA11ce);
        bob = address(0xB0b);

        planner = Deploy.newPartyPlanner();

        // Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
        tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
        targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001

        // Create pools of different sizes
        pool2 = createPool(2);
        pool10 = createPool(10);
        pool20 = createPool(20);
        pool50 = createPool(50);
    }

    /// @notice Setup a flash borrower for testing
    function setupFlashBorrower() internal returns (FlashBorrower borrower) {
        // Deploy the borrower contract
        borrower = new FlashBorrower(address(pool2));

        // Mint _tokens to alice to be used for repayments and approve borrower
        IERC20[] memory tokenAddresses = pool2.allTokens();
        vm.startPrank(alice);
        for (uint256 i = 0; i < tokenAddresses.length; i++) {
            TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2);
            TestERC20(address(tokenAddresses[i])).approve(address(borrower), type(uint256).max);
        }
        vm.stopPrank();
    }

    /// @notice Helper function: perform 10 swaps back-and-forth between the first two _tokens.
    function _performSwapGasTest(PartyPool testPool) internal {
        IERC20[] memory tokens = testPool.allTokens();
        require(tokens.length >= 2, "Pool must have at least 2 tokens");

        // Ensure alice approves pool for both _tokens
        vm.prank(alice);
        TestERC20(address(tokens[0])).approve(address(testPool), type(uint256).max);
        vm.prank(alice);
        TestERC20(address(tokens[1])).approve(address(testPool), type(uint256).max);

        uint256 maxIn = 1_000;

        // Perform 10 swaps alternating directions to avoid large imbalance
        for (uint256 i = 0; i < 10; i++) {
            vm.prank(alice);
            if (i % 2 == 0) {
                // swap token0 -> token1
                testPool.swap(alice, alice, 0, 1, maxIn, 0, 0, false);
            } else {
                // swap token1 -> token0
                testPool.swap(alice, alice, 1, 0, maxIn, 0, 0, false);
            }
        }
    }

    /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 2-token pool.
    function testSwapGasPair() public {
        _performSwapGasTest(pool2);
    }

    /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool.
    function testSwapGasTen() public {
        _performSwapGasTest(pool10);
    }

    /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 20-token pool.
    function testSwapGasTwenty() public {
        _performSwapGasTest(pool20);
    }

    /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 100-token pool.
    function testSwapGasFifty() public {
        _performSwapGasTest(pool50);
    }

    /// @notice Gas measurement: perform 10 swaps back-and-forth on a 2-token stable pair (stable-path enabled)
    function testSwapGasStablePair() public {
        PartyPool stablePair = createPoolStable(2);
        _performSwapGasTest(stablePair);
    }

    /// @notice Gas-style test: alternate swapMint then burnSwap on a 2-token stable pair
    function testSwapMintBurnSwapGasStablePair() public {
        PartyPool stablePair = createPoolStable(2);
        _performSwapMintBurnSwapGasTest(stablePair);
    }

    /// @notice Combined gas test (mint then burn) on 2-token stable pair using mint() and burn().
    function testMintBurnGasStablePair() public {
        PartyPool stablePair = createPoolStable(2);
        _performMintBurnGasTest(stablePair);
    }

    /// @notice Helper function: alternate swapMint then burnSwap to keep pool size roughly stable.
    function _performSwapMintBurnSwapGasTest(PartyPool testPool) internal {
        uint256 iterations = 10;
        uint256 input = 1_000;
        IERC20[] memory tokens = testPool.allTokens();

        // Top up alice so repeated operations won't fail
        TestERC20(address(tokens[0])).mint(alice, iterations * input * 2);

        vm.startPrank(alice);
        TestERC20(address(tokens[0])).approve(address(testPool), type(uint256).max);

        for (uint256 k = 0; k < iterations; k++) {
            // Mint LP by providing single-token input; receive LP minted
            uint256 minted = testPool.swapMint(alice, alice, 0, input, 0);
            // If nothing minted (numerical edge), skip burn step
            if (minted == 0) continue;
            // Immediately burn the minted LP back to _tokens, targeting the same token index
            testPool.burnSwap(alice, alice, minted, 0, 0, false);
        }

        vm.stopPrank();
    }

    /// @notice Gas-style test: alternate swapMint then burnSwap on the 2-token pool to keep pool size roughly stable.
    function testSwapMintBurnSwapGasPair() public {
        _performSwapMintBurnSwapGasTest(pool2);
    }

    /// @notice Gas-style test: alternate swapMint then burnSwap on the 10-token pool to keep pool size roughly stable.
    function testSwapMintBurnSwapGasTen() public {
        _performSwapMintBurnSwapGasTest(pool10);
    }

    /// @notice Gas-style test: alternate swapMint then burnSwap on the 20-token pool to keep pool size roughly stable.
    function testSwapMintBurnSwapGasTwenty() public {
        _performSwapMintBurnSwapGasTest(pool20);
    }

    /// @notice Gas-style test: alternate swapMint then burnSwap on the 100-token pool to keep pool size roughly stable.
    function testSwapMintBurnSwapGasFifty() public {
        _performSwapMintBurnSwapGasTest(pool50);
    }

    /// @notice Helper function: combined gas test (mint then burn) using mint() and burn().
    /// Alternates minting a tiny LP amount and immediately burning the actual minted LP back to avoid net pool depletion.
    function _performMintBurnGasTest(PartyPool testPool) internal {
        uint256 iterations = 50;
        uint256 input = 1_000;
        IERC20[] memory poolTokens = testPool.allTokens();

        vm.startPrank(alice);

        // Mint additional _tokens to alice and approve pool to transfer _tokens for proportional mint
        for (uint256 i = 0; i < poolTokens.length; i++) {
            TestERC20(address(poolTokens[i])).mint(alice, iterations * input * 2);
            TestERC20(address(poolTokens[i])).approve(address(testPool), type(uint256).max);
        }

        for (uint256 k = 0; k < iterations; k++) {
            // Request a tiny LP mint (1 wei) - pool will compute deposits and transfer from alice
            uint256 lpRequest = 1;

            // Snapshot alice LP before to compute actual minted
            uint256 lpBefore = testPool.balanceOf(alice);

            // Perform mint; this will transfer underlying from alice into pool
            testPool.mint(alice, alice, lpRequest, 0);

            uint256 lpAfter = testPool.balanceOf(alice);
            uint256 actualMinted = lpAfter - lpBefore;

            // If nothing minted due to rounding edge, skip burn
            if (actualMinted == 0) {
                continue;
            }

            // Burn via plain burn() which will transfer underlying back to alice and burn LP
            testPool.burn(alice, alice, actualMinted, 0, false);
        }

        vm.stopPrank();
    }

    /// @notice Combined gas test (mint then burn) on 2-token pool using mint() and burn().
    /// Alternates minting a tiny LP amount and immediately burning the actual minted LP back to avoid net pool depletion.
    function testMintBurnGasPair() public {
        _performMintBurnGasTest(pool2);
    }

    /// @notice Combined gas test (mint then burn) on 10-token pool using mint() and burn().
    /// Alternates small mints and burns to keep the pool size roughly stable.
    function testMintBurnGasTen() public {
        _performMintBurnGasTest(pool10);
    }

    /// @notice Combined gas test (mint then burn) on 20-token pool using mint() and burn().
    /// Alternates small mints and burns to keep the pool size roughly stable.
    function testMintBurnGasTwenty() public {
        _performMintBurnGasTest(pool20);
    }

    /// @notice Combined gas test (mint then burn) on 100-token pool using mint() and burn().
    /// Alternates small mints and burns to keep the pool size roughly stable.
    function testMintBurnGasFifty() public {
        _performMintBurnGasTest(pool50);
    }

    /// @notice Gas measurement: flash with single token
    function testFlashGasSingleToken() public {
        FlashBorrower borrower = setupFlashBorrower();

        // Configure borrower
        borrower.setAction(FlashBorrower.Action.NORMAL, alice);

        // Get first token from pool
        IERC20[] memory poolTokens = pool2.allTokens();
        address token = address(poolTokens[0]);
        uint256 amount = 1000;

        // Execute flash loan 10 times to measure gas
        for (uint256 i = 0; i < 10; i++) {
            pool2.flashLoan(borrower, token, amount, "");
        }
    }
}
