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

import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import "../src/LMSRStabilized.sol";
import "../src/LMSRStabilizedBalancedPair.sol";


/// @notice Forge tests for LMSRStabilized
contract LMSRStabilizedTest is Test {
    using LMSRStabilized for LMSRStabilized.State;
    using ABDKMath64x64 for int128;

    LMSRStabilized.State internal s;

    int128 stdTradeSize;
    int128 stdSlippage;


    function setUp() public {
        // 0.10% slippage when taking 1.00% of the assets
        stdTradeSize = ABDKMath64x64.divu(100,10_000);
        stdSlippage = ABDKMath64x64.divu(10,10_000);
    }

    function initBalanced() internal {
        int128[] memory q = new int128[](3);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        q[2] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);
    }

    function initAlmostBalanced() internal {
        int128[] memory q = new int128[](3);
        q[0] = ABDKMath64x64.fromUInt(999_999);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        q[2] = ABDKMath64x64.fromUInt(1_000_001);
        s.init(q, stdTradeSize, stdSlippage);
    }

    function initImbalanced() internal {
        int128[] memory q = new int128[](4);
        q[0] = ABDKMath64x64.fromUInt(1);
        q[1] = ABDKMath64x64.fromUInt(1e9);
        q[2] = ABDKMath64x64.fromUInt(1);
        q[3] = ABDKMath64x64.divu(1, 1e9);
        s.init(q, stdTradeSize, stdSlippage);
    }


    function testInitBalanced() public {
        // Test 1: Balanced Pool Initialization
        initBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000); 
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // Verify slippage by performing asset swaps and checking price impact
        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        // For a balanced pool, test asset 0 -> asset 1 swap
        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Verify amountIn and amountOut are reasonable
        assertTrue(amountIn > 0, "amountIn should be positive");
        assertTrue(amountOut > 0, "amountOut should be positive");

        // Calculate slippage = (initialPrice/finalPrice - 1)
        // Compute e values dynamically for price ratio
        int128 b = _computeB(mockQInternal);
        int128[] memory eValues = _computeE(b, mockQInternal);

        // For balanced pool, initial price ratio is 1:1
        int128 initialRatio = eValues[0].div(eValues[1]);

        // Verify initial ratio for balanced pool is approximately 1:1
        assertTrue((initialRatio.sub(ABDKMath64x64.fromInt(1))).abs() < ABDKMath64x64.divu(1, 10000), 
            "Initial price ratio should be close to 1:1");

        // After trade, the new e values would be different
        int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
        int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
        int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
        console2.log('slippage', slippage);

        // Slippage should be close to stdSlippage (within 1% relative error)
        int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
        assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Balanced pool slippage error too high");
    }

    function testInitAlmostBalanced() public {
        // Test 2: Almost Balanced Pool Initialization
        initAlmostBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(999_999);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_001);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // Verify slippage for almost balanced pool
        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Verify amountIn and amountOut are reasonable
        assertTrue(amountIn > 0, "amountIn should be positive");
        assertTrue(amountOut > 0, "amountOut should be positive");

        // Compute e values dynamically for price ratio
        int128 b = _computeB(mockQInternal);
        int128[] memory eValues = _computeE(b, mockQInternal);
        int128 initialRatio = eValues[0].div(eValues[1]);
        int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
        // Verify the initial ratio is close to but not exactly 1:1
        assertTrue(relDiff < ABDKMath64x64.divu(1, 1000),
            "Initial ratio should be close to 1:1 for almost balanced pool");
        assertTrue(relDiff > ABDKMath64x64.divu(1, 10000000),
            "Initial ratio should not be exactly 1:1 for almost balanced pool");

        int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
        int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
        int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
        console2.log('slippage', slippage);
        int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
        assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Almost balanced pool slippage error too high");
    }

    function testInitImbalanced() public {
        // Test 3: Imbalanced Pool Initialization
        initImbalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](4);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1e9);
        mockQInternal[2] = ABDKMath64x64.fromUInt(1);
        mockQInternal[3] = ABDKMath64x64.divu(1, 1e9);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // For imbalanced pool, we need to try an "average" swap
        // We'll use asset 0 -> asset 2 as it's more balanced than asset 0 -> asset 1
        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        // Compute e values dynamically for price ratio
        int128 b = _computeB(mockQInternal);
        int128[] memory eValues = _computeE(b, mockQInternal);

        // Verify the ratios between small and large assets is different
        int128 initialRatio = eValues[0].div(eValues[3]); // Assets 0 and 2 match, and assets 1 and 3 match. 0 and 3 differ.
        int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
        // Verify initial ratio shows significant imbalance
        assertTrue(relDiff != 0, "Initial ratio should show imbalance");

        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 2, tradeAmount, 0);

        // Verify amountIn and amountOut are reasonable
        assertTrue(amountIn > 0, "amountIn should be positive");
        assertTrue(amountOut > 0, "amountOut should be positive");

        int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
        int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2]));
        int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
        console2.log('slippage', slippage);

        // Since the imbalance is extreme, with one coin worth lots more than the others, the actual slippage for
        // this swap is actually off by about 100%
        // When we configure kappa, it is a best case slippage (worst case AMM loss) that only occurs with balanced
        // assets
        int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
        console2.log('relative error', relativeError);
        assertLt(relativeError, ABDKMath64x64.divu(100, 100), "Imbalanced pool slippage error too high");
    }

    function testRecentering() public {
        // Recentering functionality has been removed since we no longer cache intermediate values
        // This test is now a no-op but kept for API compatibility
        initAlmostBalanced();

        // Verify basic state is still functional
        assertTrue(s.nAssets > 0, "State should still be initialized");
        assertTrue(s.kappa > int128(0), "Kappa should still be positive");
    }

    function testRescalingAfterDeposit() public {
        // Initialize pool with almost balanced assets
        initAlmostBalanced();

        // Create initial asset quantities
        int128[] memory initialQ = new int128[](3);
        initialQ[0] = ABDKMath64x64.fromUInt(999_999);
        initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQ);

        // Store initial parameters
        int128 initialB = _computeB(initialQ);
        int128 initialKappa = s.kappa;

        // Simulate a deposit by increasing all asset quantities by 50%
        int128[] memory newQ = new int128[](s.nAssets);
        for (uint i = 0; i < s.nAssets; i++) {
            // Increase by 50%
            newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2))); // 1.5x
        }

        // Apply the update for proportional change
        s.updateForProportionalChange(newQ);

        // Verify that b has been rescaled proportionally
        int128 newB = _computeB(s.qInternal);
        int128 expectedRatio = ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2)); // 1.5x
        int128 actualRatio = newB.div(initialB);

        int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
        assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after deposit");

        // Verify kappa remained unchanged
        assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after deposit");

        // Verify slippage target is still met by performing a trade
        int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Verify computed swap amounts
        assertTrue(amountIn > 0, "Swap amountIn should be positive");
        assertTrue(amountOut > 0, "Swap amountOut should be positive");
        // Verify amountOut is reasonable compared to amountIn (not a severe loss)
        assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");

        int128[] memory eValues = _computeE(newB, s.qInternal);
        int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
        int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
        int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
        console2.log('post-deposit slippage', slippage);

        int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
        assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit");
    }

    /// @notice Test balanced2 handling of limitPrice that causes truncation of input a
    function testBalanced2LimitTruncation() public {
        // Two-asset balanced pool
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        // Compute b for constructing meaningful a and limits
        int128 b = _computeB(q);

        // Choose a large requested input so that the limitPrice will truncate it.
        int128 aRequested = b.mul(ABDKMath64x64.fromUInt(10)); // a/b = 10 (within EXP_LIMIT)

        // Small limit slightly above current price (r0 == 1). Use 0.1% above parity.
        int128 limitPrice = ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000)); // 1.001

        // Call optimized balanced2 and exact versions
        (int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, aRequested, limitPrice);
        (int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, aRequested, limitPrice);

        // Ensure exact returned something sensible
        assertTrue(outExact > 0, "exact output should be positive");

        // Relative error tolerance 0.001% (1e-5)
        int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);
        int128 tol = ABDKMath64x64.divu(1, 100_000);
        assertLt(relErr, tol, "balanced2 truncated output deviates from exact beyond tolerance");

        // Input used should be close as well
        int128 inRelErr = (inApprox.sub(inExact)).abs();
        // If exact truncated, inExact likely equals aLimit computed by ln; allow small absolute difference tolerance of 1e-6 relative to b
        int128 absTol = b.div(ABDKMath64x64.fromUInt(1_000_000)); // b * 1e-6
        assertTrue(inRelErr <= absTol, "balanced2 truncated input differs from exact beyond small absolute tolerance");
    }

    /// @notice Test balanced2 with a limitPrice that does not truncate the provided input
    function testBalanced2LimitNoTruncation() public {
        // Two-asset balanced pool
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        // Small input a
        int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset

        // Very relaxed limit (2x current price) which should not truncate
        int128 limitPrice = ABDKMath64x64.fromUInt(2);

        (int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, limitPrice);
        (int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, limitPrice);

        // Exact outputs must be positive
        assertTrue(outExact > 0, "exact output should be positive");

        // Expect almost exact match when no truncation occurs; use tight tolerance
        int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);
        int128 tol = ABDKMath64x64.divu(1, 100_000); // 0.001%
        assertLt(relErr, tol, "balanced2 no-truncate output deviates from exact beyond tolerance");

        // AmountIn should equal provided a for both functions
        assertEq(inApprox, a, "balanced2 should use full input when not truncated");
        assertEq(inExact, a, "exact should use full input when not truncated");
    }

    /// @notice Test that balanced2 reverts when limitPrice <= current price (no partial fill allowed)
    function testBalanced2LimitRevertWhenAtOrBelowCurrent() public {
        // Two-asset balanced pool
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        int128 limitPrice = ABDKMath64x64.fromInt(1); // equal to current price

        vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
        this._swapAmountsForExactInput_balanced2(0, 1, q[0].mul(ABDKMath64x64.divu(1, 1000)), limitPrice);
    }

    function _swapAmountsForExactInput_balanced2(
        uint256 i,
        uint256 j,
        int128 a,
        int128 limitPrice
    ) external view returns (int128 amountIn, int128 amountOut) {
        return LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, i,j,a,limitPrice);
    }


    function testRescalingAfterWithdrawal() public {
        // Initialize pool with almost balanced assets
        initAlmostBalanced();

        // Create initial asset quantities
        int128[] memory initialQ = new int128[](3);
        initialQ[0] = ABDKMath64x64.fromUInt(999_999);
        initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQ);

        // Store initial parameters
        int128 initialB = _computeB(initialQ);
        int128 initialKappa = s.kappa;

        // Simulate a withdrawal by decreasing all asset quantities by 30%
        int128[] memory newQ = new int128[](s.nAssets);
        for (uint i = 0; i < s.nAssets; i++) {
            // Decrease by 30%
            newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10))); // 0.7x
        }

        // Apply the update for proportional change
        s.updateForProportionalChange(newQ);

        // Verify that b has been rescaled proportionally
        int128 newB = _computeB(s.qInternal);
        int128 expectedRatio = ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10)); // 0.7x
        int128 actualRatio = newB.div(initialB);

        int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
        assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after withdrawal");

        // Verify kappa remained unchanged
        assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after withdrawal");

        // Verify slippage target is still met by performing a trade
        int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Verify computed swap amounts
        assertTrue(amountIn > 0, "Swap amountIn should be positive");
        assertTrue(amountOut > 0, "Swap amountOut should be positive");
        // Verify amountOut is reasonable compared to amountIn (not a severe loss)
        assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");

        int128[] memory eValues = _computeE(newB, s.qInternal);
        int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
        int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
        int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
        console2.log('post-withdrawal slippage', slippage);

        int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
        assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal");
    }

    // --- tests probing numerical stability and boundary conditions ---

    /// @notice Recentering functionality has been removed - this test is now a no-op
    function testRecenterShiftTooLargeReverts() public {
        initAlmostBalanced();
        // Recentering has been removed, so this test now just verifies basic functionality
        assertTrue(s.nAssets > 0, "State should still be initialized");
    }

    /// @notice limitPrice <= current price should revert (no partial fill)
    function testLimitPriceRevertWhenAtOrBelowCurrent() public {
        initBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000); 
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // For balanced pool r0 = 1. Use limitPrice == 1 which should revert.
        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
        this.externalSwapAmountsForExactInput(0, 1, tradeAmount, ABDKMath64x64.fromInt(1));
    }

    /// @notice If e_j == 0 we should revert early to avoid div-by-zero
    function testEJZeroReverts() public {
        initBalanced();

        // Create mock qInternal where asset 1 has zero quantity
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = int128(0); // Zero quantity for asset 1
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        vm.expectRevert(bytes("LMSR: e_j==0"));
        this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0);
    }

    /// @notice swapAmountsForPriceLimit returns zero if limit equals current price
    function testSwapAmountsForPriceLimitZeroWhenLimitEqualsPrice() public {
        initBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000); 
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // For balanced pool r0 = 1. swapAmountsForPriceLimit with limit==1 should be zero
        vm.expectRevert('LMSR: limitPrice <= current price');
        this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1));

        // Try with a limit price slightly above 1, which should not revert
        try this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000))) returns (int128 _amountIn, int128 _maxOut) {
            // Verify that the returned values are reasonable
            assertTrue(_amountIn > 0, "amountIn should be positive for valid limit price");
            assertTrue(_maxOut > 0, "maxOut should be positive for valid limit price");
        } catch {
            fail("Should not revert with limit price > current price");
        }
    }

    function externalSwapAmountsForPriceLimit(uint256 i, uint256 j, int128 limitPrice) external view
    returns (int128, int128) {
        return s.swapAmountsForPriceLimit(i, j, limitPrice);
    }

    /// @notice Gas/throughput test: perform 100 alternating swaps between asset 0 and 1
    function testSwapGas() public {
        // Initialize the almost-balanced pool
        initAlmostBalanced();

        // Create mock qInternal that we'll update through swaps
        int128[] memory currentQ = new int128[](3);
        currentQ[0] = ABDKMath64x64.fromUInt(999_999);
        currentQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        currentQ[2] = ABDKMath64x64.fromUInt(1_000_001);

        // Update the state's cached qInternal
        _updateCachedQInternal(currentQ);

        // Perform 100 swaps, alternating between asset 0 -> 1 and 1 -> 0
        for (uint256 iter = 0; iter < 100; iter++) {
            uint256 from = (iter % 2 == 0) ? 0 : 1;
            uint256 to = (from == 0) ? 1 : 0;

            // Use standard trade size applied to the 'from' asset's current quantity
            int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);

            // Compute swap amounts and apply to state
            (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);

            // applySwap now updates the internal qInternal directly
            s.applySwap(from, to, amountIn, amountOut);
        }
    }

    /// @notice Extremely large a that makes a/b exceed expLimit should revert
    function testAmountOutABOverflowReverts() public {
        initBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000); 
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        int128 b = _computeB(mockQInternal);
        // Pick a such that a/b = 33 (expLimit is 32). a = b * 33
        int128 aOverB_target = ABDKMath64x64.fromInt(33);
        int128 a = b.mul(aOverB_target);

        vm.expectRevert(bytes("LMSR: a/b too large (would overflow exp)"));
        this.externalSwapAmountsForExactInput(0, 1, a, 0);
    }

    // Helper function to compute b from qInternal (either from provided array or state)
    function _computeB(int128[] memory qInternal) internal view returns (int128) {
        int128 sizeMetric = _computeSizeMetric(qInternal);
        require(sizeMetric > int128(0), "LMSR: size metric zero");
        return s.kappa.mul(sizeMetric);
    }

    // Overload that uses state's cached qInternal
    function _computeB() internal view returns (int128) {
        int128 sizeMetric = _computeSizeMetric(s.qInternal);
        require(sizeMetric > int128(0), "LMSR: size metric zero");
        return s.kappa.mul(sizeMetric);
    }

    // Helper function to compute size metric (sum of all asset quantities)
    function _computeSizeMetric(int128[] memory qInternal) internal pure returns (int128) {
        int128 total = int128(0);
        for (uint i = 0; i < qInternal.length; ) {
            total = total.add(qInternal[i]);
            unchecked { i++; }
        }
        return total;
    }

    // Helper function to update the state's cached qInternal
    function _updateCachedQInternal(int128[] memory mockQInternal) internal {
        // First ensure qInternal array exists with the right size
        if (s.qInternal.length != mockQInternal.length) {
            s.qInternal = new int128[](mockQInternal.length);
        }

        // Copy values from mockQInternal to state's qInternal
        for (uint i = 0; i < mockQInternal.length; ) {
            s.qInternal[i] = mockQInternal[i];
            unchecked { i++; }
        }
    }

    // Helper function to compute M and Z dynamically
    function _computeMAndZ(int128 b, int128[] memory qInternal) internal pure returns (int128 M, int128 Z) {
        require(qInternal.length > 0, "LMSR: no assets");

        // Compute y_i = q_i / b for numerical stability
        int128[] memory y = new int128[](qInternal.length);
        for (uint i = 0; i < qInternal.length; ) {
            y[i] = qInternal[i].div(b);
            unchecked { i++; }
        }

        // Find max y for centering (M = maxY)
        M = y[0];
        for (uint i = 1; i < qInternal.length; ) {
            if (y[i] > M) M = y[i];
            unchecked { i++; }
        }

        // Compute Z = sum of exp(z_i) where z_i = y_i - M
        Z = int128(0);
        for (uint i = 0; i < qInternal.length; ) {
            int128 z_i = y[i].sub(M);
            int128 e_i = _exp(z_i);
            Z = Z.add(e_i);
            unchecked { i++; }
        }
    }

    // Helper function to compute all e[i] = exp(z[i]) values dynamically
    function _computeE(int128 b, int128[] memory qInternal) internal pure returns (int128[] memory e) {
        (int128 M, ) = _computeMAndZ(b, qInternal);
        e = new int128[](qInternal.length);

        for (uint i = 0; i < qInternal.length; ) {
            int128 y_i = qInternal[i].div(b);
            int128 z_i = y_i.sub(M);
            e[i] = _exp(z_i);
            unchecked { i++; }
        }
    }

    // Helper function to calculate exp (copied from LMSRStabilized library)
    function _exp(int128 x) internal pure returns (int128) {
        return ABDKMath64x64.exp(x);
    }

    // External helper function that wraps swapAmountsForExactInput to properly handle reverts in tests
    function externalSwapAmountsForExactInput(
        uint i,
        uint j,
        int128 a,
        int128 limitPrice
    ) external view returns (int128 amountIn, int128 amountOut) {
        return s.swapAmountsForExactInput(i, j, a, limitPrice);
    }

    // External helper function that wraps recenterIfNeeded to properly handle reverts in tests
    function externalRecenterIfNeeded() external {
        // Recentering has been removed - this is now a no-op
    }

    // External helper function that wraps applySwap to properly handle reverts in tests
    function externalApplySwap(
        uint i,
        uint j,
        int128 amountIn,
        int128 amountOut
    ) external {
        s.applySwap(i, j, amountIn, amountOut);
    }

    // Small helper: convert a Q64.64 int128 into micro-units (value * 1e6) as an int256 for readable logging.
    // Example: if x represents 0.001 (Q64.64), _toMicro(x) will return ~1000.
    function _toMicro(int128 x) internal pure returns (int256) {
        int256 ONE = int256(uint256(0x10000000000000000)); // 2^64
        return (int256(x) * 1_000_000) / ONE;
    }

    /// @notice Test that applySwap correctly validates swap parameters and updates qInternal
    function testApplySwap() public {
        // Initialize with balanced assets
        initBalanced();

        // Create mock qInternal for testing
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000); 
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(mockQInternal);

        // Save original values for comparison
        int128 originalQ0 = s.qInternal[0];
        int128 originalQ1 = s.qInternal[1];

        // Calculate swap amounts from asset 0 to asset 1
        int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);

        (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Verify basic swap calculation worked
        assertTrue(amountIn > 0, "amountIn should be positive");
        assertTrue(amountOut > 0, "amountOut should be positive");

        // Apply the swap - should not revert for valid inputs
        s.applySwap(0, 1, amountIn, amountOut);

        // Verify qInternal is correctly updated
        // Input asset should increase by amountIn
        assertEq(s.qInternal[0], originalQ0.add(amountIn), "qInternal[0] should be updated");
        // Output asset should decrease by amountOut
        assertEq(s.qInternal[1], originalQ1.sub(amountOut), "qInternal[1] should be updated");
    }

    /// @notice Test path independence by comparing direct vs indirect swaps
    function testPathIndependence() public {
        // Start with a balanced pool
        initBalanced();

        // Create initial quantities
        int128[] memory initialQValues = new int128[](s.nAssets);
        initialQValues[0] = ABDKMath64x64.fromUInt(1_000_000);
        initialQValues[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQValues[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQValues);

        // Test path independence by computing swap outcomes without state changes
        int128 directSwapAmount = initialQValues[0].mul(stdTradeSize);

        // Store a backup of the original values to restore between swaps
        int128[] memory backupQ = new int128[](s.nAssets);
        for (uint i = 0; i < s.nAssets; i++) {
            backupQ[i] = s.qInternal[i];
        }

        // Path 1: Direct swap from asset 0 to asset 2
        (/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);

        // Restore original state for second path
        _updateCachedQInternal(backupQ);

        // Path 2: Swap from asset 0 to asset 1, then from asset 1 to asset 2
        (int128 indirectAmountIn1, int128 indirectAmountOut1) = s.swapAmountsForExactInput(0, 1, directSwapAmount, 0);

        // Update state for second leg of indirect path
        s.qInternal[0] = s.qInternal[0].sub(indirectAmountIn1);
        s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);

        // Second swap: asset 1 -> asset 2
        (/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);

        // The path independence property isn't perfect due to discrete swap mechanics,
        // but the difference should be within reasonable bounds
        console2.log("Direct swap output:");
        console2.logInt(directAmountOut);
        console2.log("Indirect swap total output:");
        console2.logInt(indirectAmountOut2);

        // Basic verification that both paths produce positive outputs
        assertTrue(directAmountOut > 0, "Direct swap should produce positive output");
        assertTrue(indirectAmountOut2 > 0, "Indirect swap should produce positive output");
    }

    /// @notice Test round-trip trades to verify near-zero slippage
    function testRoundTripTradesAcrossAllPools() public {
        // Test with balanced pool only since we removed state caching
        initBalanced();

        // Create mock qInternal
        int128[] memory initialQ = new int128[](3);
        initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQ);

        console2.log("Testing round-trip trades for balanced pool");

        // Use standard trade size
        int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);

        // Step 1: Swap asset 0 -> asset 1
        (int128 amountIn1, int128 amountOut1) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);

        // Update quantities for step 2
        s.qInternal[0] = s.qInternal[0].sub(amountIn1);
        s.qInternal[1] = s.qInternal[1].add(amountOut1);

        // Step 2: Swap back asset 1 -> asset 0
        (/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);

        // Calculate round-trip slippage: (initial amount - final amount) / initial amount
        int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);

        console2.log("Round-trip slippage (micro-units):");
        console2.logInt(_toMicro(roundTripSlippage));

        // Verify round-trip slippage is reasonable
        int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance
        assertLt(roundTripSlippage.abs(), tolerance, "Round-trip slippage should be near zero");
    }

    /// @notice Test that slippage is approximately equal in both directions for small swaps
    function testBidirectionalSlippageSymmetry() public {
        // Initialize with balanced assets for clearest slippage measurement
        initBalanced();

        // Create mock qInternal
        int128[] memory initialQ = new int128[](3);
        initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQ);

        // Use small trade size for clear slippage measurement
        int128 tradeSize = ABDKMath64x64.divu(5, 10_000); // 0.05% of pool
        int128 tradeAmount0 = s.qInternal[0].mul(tradeSize);
        int128 tradeAmount1 = s.qInternal[1].mul(tradeSize);

        // Store original state to restore between tests
        int128[] memory backupQ = new int128[](s.nAssets);
        for (uint i = 0; i < s.nAssets; i++) {
            backupQ[i] = s.qInternal[i];
        }

        // First direction: asset 0 -> asset 1
        (int128 amountIn0to1, int128 amountOut0to1) = s.swapAmountsForExactInput(0, 1, tradeAmount0, 0);

        // Restore original state
        _updateCachedQInternal(backupQ);

        // Second direction: asset 1 -> asset 0  
        (int128 amountIn1to0, int128 amountOut1to0) = s.swapAmountsForExactInput(1, 0, tradeAmount1, 0);

        console2.log("0->1 swap amountIn:");
        console2.logInt(amountIn0to1);
        console2.log("0->1 swap amountOut:");
        console2.logInt(amountOut0to1);
        console2.log("1->0 swap amountIn:");
        console2.logInt(amountIn1to0);
        console2.log("1->0 swap amountOut:");
        console2.logInt(amountOut1to0);

        // For balanced pools, the swap ratios should be approximately symmetric
        int128 ratio0to1 = amountOut0to1.div(amountIn0to1);
        int128 ratio1to0 = amountOut1to0.div(amountIn1to0);

        // Calculate relative difference between the ratios
        int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs();
        int128 relativeRatioDiff = ratioDifference.div(ratio0to1.add(ratio1to0).div(ABDKMath64x64.fromInt(2)));

        console2.log("Relative ratio difference (micro-units):");
        console2.logInt(_toMicro(relativeRatioDiff));

        // Assert that the relative difference between ratios is small
        int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance
        assertLt(relativeRatioDiff, tolerance,
            "Swap ratios should be approximately equal in both directions");
    }

    /// @notice Test that basic swap functionality works across multiple operations
    function testZConsistencyAfterMultipleSwaps() public {
        // Initialize with balanced assets
        initBalanced();

        // Create mock qInternal that we'll update through swaps
        int128[] memory initialQ = new int128[](3);
        initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
        initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);

        // Update the state's cached qInternal
        _updateCachedQInternal(initialQ);

        // Perform multiple swaps in different directions
        for (uint i = 0; i < 5; i++) {
            // Swap from asset i%3 to asset (i+1)%3
            uint from = i % 3;
            uint to = (i + 1) % 3;

            int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);

            (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);

            // Apply swap to update internal state
            s.applySwap(from, to, amountIn, amountOut);

            // Basic validation that swap worked
            assertTrue(amountIn > 0, "amountIn should be positive");
            assertTrue(amountOut > 0, "amountOut should be positive");
        }
    }

    // --- New tests for single-token mint/burn helpers ---

    /// @notice Basic sanity check for swapAmountsForMint: small single-token input
    function testSwapAmountsForMintBasic() public {
        initBalanced();

        // Use a small single-token input (stdTradeSize fraction of asset 0)
        int128 a = s.qInternal[0].mul(stdTradeSize);

        (int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);

        // consumed must be non-negative and <= provided a (partial-fill allowed)
        assertTrue(consumed > 0, "consumed should be positive");
        assertTrue(consumed <= a, "consumed should not exceed provided input");

        // lpIncrease should be positive
        assertTrue(lpIncrease > 0, "lpIncrease should be positive");
    }

    /// @notice Large input for swapAmountsForMint should return a valid partial fill (consumed <= provided)
    function testSwapAmountsForMintLargeInputPartial() public {
        initAlmostBalanced();

        // Provide a large input far above stdTradeSize to exercise cap logic
        int128 a = s.qInternal[0].mul(ABDKMath64x64.fromUInt(1000)); // 1000x one-asset quantity

        (int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);

        // Should not consume more than provided
        assertTrue(consumed <= a, "consumed must be <= provided");

        // If nothing could be consumed, the helper should revert earlier; otherwise positive
        assertTrue(consumed > 0, "consumed should be positive for large input in normal pools");
        assertTrue(lpIncrease > 0, "lpIncrease should be positive for large input");
    }

    /// @notice Basic swapAmountsForBurn sanity: small alpha should return positive single-asset payout
    function testSwapAmountsForBurnBasic() public {
        initBalanced();

        // Burn alpha fraction of pool
        int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
        int128 S = _computeSizeMetric(s.qInternal);

        (int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha);

        // burned should equal alpha * S
        assertEq(burned, alpha.mul(S), "burned size-metric mismatch");

        // payout should be positive
        assertTrue(payout > 0, "payout must be positive for balanced pool burn");
    }

    /// @notice If some assets have zero quantity, burn should skip them but still return payout when possible
    function testSwapAmountsForBurnWithZeroAsset() public {
        initBalanced();

        // Make asset 1 empty; others non-zero
        int128[] memory mockQInternal = new int128[](3);
        mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
        mockQInternal[1] = int128(0); // zero
        mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
        _updateCachedQInternal(mockQInternal);

        int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
        (int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha);

        // Should still burn the size metric
        int128 S = _computeSizeMetric(mockQInternal);
        assertEq(burned, alpha.mul(S), "burned size-metric mismatch with zero asset present");

        // Payout should be at least the direct redeemed portion (alpha * q_i)
        assertTrue(payout >= alpha.mul(mockQInternal[0]), "payout should be >= direct redeemed portion");

        // Payout must be positive
        assertTrue(payout > 0, "payout must be positive even when one asset is zero");
    }

    /// @notice Test that the balanced2 polynomial approximation is accurate for a two-asset balanced pool
    function testBalanced2ApproxAccuracy() public {
        // Create a minimal two-asset balanced pool
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        // Small trade (well within u <= 0.5 and delta <= 1%)
        int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset

        // Compute approx and exact
        (int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
        (int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);

        // Sanity
        assertTrue(outExact > 0, "Exact output should be positive");

        // Relative error: |approx - exact| / exact
        int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);

        // Require relative error < 0.001% (1e-5) => expressed as 1 / 100_000
        int128 tolerance = ABDKMath64x64.divu(1, 100_000);
        assertLt(relErr, tolerance, "balanced2 approximation relative error too large");

        // AmountIn should equal requested a (no truncation)
        assertEq(inApprox, a, "balanced2 approximation should use full input when no limitPrice");
        assertEq(inExact, a, "exact computation should use full input when no limitPrice");
    }

    /// @notice Test that when the parity assumption is violated, the balanced2 helper falls back
    /// to the exact implementation (we expect identical outputs).
    function testBalanced2FallbackWhenParityViolated() public {
        // Start with two-asset balanced pool (we'll mutate it)
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        // Prepare newQ starting from equal quantities; we'll grow q0 until delta > DELTA_MAX
        int128[] memory newQ = new int128[](2);
        newQ[0] = q[0];
        newQ[1] = q[1];

        // DELTA_MAX used by the library: 0.01
        int128 DELTA_MAX = ABDKMath64x64.divu(1, 100);

        // Iteratively increase q0 until the library's delta = (q0 - q1) / b > DELTA_MAX
        // We cap iterations to avoid infinite loops in pathological cases.
        bool reached = false;
        for (uint iter = 0; iter < 64; iter++) {
            // Update the state's cached qInternal with the candidate imbalance
            _updateCachedQInternal(newQ);

            // Compute the current b and delta using the state's parameters
            int128 bNow = _computeB(); // uses s.qInternal and s.kappa
            // avoid division by zero
            if (bNow == int128(0)) { break; }

            int128 deltaNow = newQ[0].sub(newQ[1]).div(bNow);
            if (deltaNow < int128(0)) { deltaNow = deltaNow.neg(); }

            if (deltaNow > DELTA_MAX) {
                reached = true;
                break;
            }

            // Increase q0 by 10% each iteration to quickly cross the threshold
            newQ[0] = newQ[0].mul(ABDKMath64x64.fromUInt(11)).div(ABDKMath64x64.fromUInt(10));
        }

        // Ensure we actually achieved the desired imbalance for a meaningful test
        _updateCachedQInternal(newQ);
        int128 finalB = _computeB();
        int128 finalDelta = newQ[0].sub(newQ[1]).div(finalB);
        if (finalDelta < int128(0)) finalDelta = finalDelta.neg();
        assertTrue(finalDelta > DELTA_MAX, "failed to create delta > DELTA_MAX in test");

        // Small trade amount
        int128 a = newQ[0].mul(ABDKMath64x64.divu(1, 1000));

        // Call both functions; balanced2 should detect parity violation and fall back to exact
        (int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
        (int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);

        // Because parity assumption is violated balanced2 should fall back to exact implementation
        assertEq(inApprox, inExact, "fallback should return identical amountIn");
        assertEq(outApprox, outExact, "fallback should return identical amountOut");
    }

    /// @notice Test that the balanced2 helper falls back when scaled input u = a/b is too large
    function testBalanced2FallbackOnLargeInput() public {
        // Two-asset balanced pool
        int128[] memory q = new int128[](2);
        q[0] = ABDKMath64x64.fromUInt(1_000_000);
        q[1] = ABDKMath64x64.fromUInt(1_000_000);
        s.init(q, stdTradeSize, stdSlippage);

        // Compute b
        int128 b = _computeB(q);

        // Choose a so that u = a / b = 0.75 (> 0.5 U_MAX)
        int128 a = b.mul(ABDKMath64x64.divu(3, 4)); // a/b = 0.75

        // Call both functions and expect fallback (identical results)
        (int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
        (int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);

        assertEq(inApprox, inExact, "fallback on large input should return identical amountIn");
        assertEq(outApprox, outExact, "fallback on large input should return identical amountOut");
    }

}
