// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;

import "../../src/libraries/UniswapMath.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import "forge-std/Test.sol";

/**
 * @title UniswapMath Test Suite
 * @notice Unit tests for mathematical utilities used in Uniswap V3 calculations
 */
contract MockUniswapMath is UniswapMath {
    // Expose internal functions for testing
    function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) external pure returns (int24) {
        return _tickAtPrice(t0isWeth, tokenAmount, ethAmount);
    }

    function tickAtPriceRatio(int128 priceRatioX64) external pure returns (int24) {
        return _tickAtPriceRatio(priceRatioX64);
    }

    function priceAtTick(int24 tick) external pure returns (uint256) {
        return _priceAtTick(tick);
    }

    function clampToTickSpacing(int24 tick, int24 spacing) external pure returns (int24) {
        return _clampToTickSpacing(tick, spacing);
    }
}

contract UniswapMathTest is Test {
    MockUniswapMath internal uniswapMath;

    int24 internal constant TICK_SPACING = 200;

    function setUp() public {
        uniswapMath = new MockUniswapMath();
    }

    // ========================================
    // TICK AT PRICE TESTS
    // ========================================

    function testTickAtPriceBasic() public {
        // Test 1:1 ratio (equal amounts)
        uint256 tokenAmount = 1 ether;
        uint256 ethAmount = 1 ether;

        int24 tickWethToken0 = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);
        int24 tickTokenToken0 = uniswapMath.tickAtPrice(false, tokenAmount, ethAmount);

        // Ticks should be opposite signs for different token orderings
        assertEq(tickWethToken0, -tickTokenToken0, "Ticks should be negatives of each other");
        assertGt(tickWethToken0, -1000, "Tick should be reasonable for 1:1 ratio");
        assertLt(tickWethToken0, 1000, "Tick should be reasonable for 1:1 ratio");
    }

    function testTickAtPriceZeroToken() public {
        // When token amount is 0, should return MAX_TICK
        int24 tick = uniswapMath.tickAtPrice(true, 0, 1 ether);
        assertEq(tick, TickMath.MAX_TICK, "Zero token amount should return MAX_TICK");
    }

    function testTickAtPriceZeroEthReverts() public {
        // When ETH amount is 0, should revert
        vm.expectRevert("ETH amount cannot be zero");
        uniswapMath.tickAtPrice(true, 1 ether, 0);
    }

    function testTickAtPriceHighRatio() public {
        // Test when token is much more expensive than ETH
        uint256 tokenAmount = 1 ether;
        uint256 ethAmount = 1000 ether; // Token is cheap relative to ETH

        int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);

        // Should be a large negative tick (cheap token)
        assertLt(tick, -10_000, "Cheap token should result in large negative tick");
        assertGt(tick, TickMath.MIN_TICK, "Tick should be within valid range");
    }

    function testTickAtPriceLowRatio() public {
        // Test when token is much cheaper than ETH
        uint256 tokenAmount = 1000 ether; // Token is expensive relative to ETH
        uint256 ethAmount = 1 ether;

        int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);

        // Should be a large positive tick (expensive token)
        assertGt(tick, 10_000, "Expensive token should result in large positive tick");
        assertLt(tick, TickMath.MAX_TICK, "Tick should be within valid range");
    }

    // ========================================
    // PRICE AT TICK TESTS
    // ========================================

    function testPriceAtTickZero() public {
        // Tick 0 should give price ratio of 1 (in X96 format)
        uint256 price = uniswapMath.priceAtTick(0);
        uint256 expectedPrice = 1 << 96; // 1.0 in X96 format

        assertEq(price, expectedPrice, "Tick 0 should give price ratio of 1");
    }

    function testPriceAtTickPositive() public {
        // Positive tick should give price > 1
        uint256 price = uniswapMath.priceAtTick(1000);
        uint256 basePrice = 1 << 96;

        assertGt(price, basePrice, "Positive tick should give price > 1");
    }

    function testPriceAtTickNegative() public {
        // Negative tick should give price < 1
        uint256 price = uniswapMath.priceAtTick(-1000);
        uint256 basePrice = 1 << 96;

        assertLt(price, basePrice, "Negative tick should give price < 1");
    }

    function testPriceAtTickSymmetry() public {
        // Test that positive and negative ticks are reciprocals
        int24 tick = 5000;
        uint256 pricePositive = uniswapMath.priceAtTick(tick);
        uint256 priceNegative = uniswapMath.priceAtTick(-tick);

        // pricePositive * priceNegative should approximately equal (1 << 96)^2
        uint256 product = (pricePositive >> 48) * (priceNegative >> 48); // Scale down to prevent overflow
        uint256 expected = 1 << 96;

        // Allow small tolerance for rounding errors
        assertApproxEqRel(product, expected, 0.01e18, "Positive and negative ticks should be reciprocals");
    }

    // ========================================
    // CLAMP TO TICK SPACING TESTS
    // ========================================

    function testClampToTickSpacingExact() public {
        // Test tick that's already aligned
        int24 alignedTick = 1000; // Already multiple of 200
        int24 result = uniswapMath.clampToTickSpacing(alignedTick, TICK_SPACING);

        assertEq(result, alignedTick, "Already aligned tick should remain unchanged");
    }

    function testClampToTickSpacingRoundDown() public {
        // Test tick that needs rounding down
        int24 unalignedTick = 1150; // Should round down to 1000
        int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING);

        assertEq(result, 1000, "Tick should round down to nearest multiple");
    }

    function testClampToTickSpacingRoundUp() public {
        // Test negative tick that needs rounding
        int24 unalignedTick = -1150; // Should round to -1000 (towards zero)
        int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING);

        assertEq(result, -1000, "Negative tick should round towards zero");
    }

    function testClampToTickSpacingMinBound() public {
        // Test tick below minimum
        int24 result = uniswapMath.clampToTickSpacing(TickMath.MIN_TICK - 1000, TICK_SPACING);

        assertEq(result, TickMath.MIN_TICK, "Tick below minimum should clamp to MIN_TICK");
    }

    function testClampToTickSpacingMaxBound() public {
        // Test tick above maximum
        int24 result = uniswapMath.clampToTickSpacing(TickMath.MAX_TICK + 1000, TICK_SPACING);

        assertEq(result, TickMath.MAX_TICK, "Tick above maximum should clamp to MAX_TICK");
    }

    // ========================================
    // ROUND-TRIP CONVERSION TESTS
    // ========================================

    function testTickPriceRoundTrip() public {
        // Test that tick → price → tick preserves the original value
        int24 originalTick = 12_345;
        originalTick = uniswapMath.clampToTickSpacing(originalTick, TICK_SPACING); // Align to spacing

        uint256 price = uniswapMath.priceAtTick(originalTick);

        // Note: Direct round-trip through tickAtPriceRatio isn't possible since
        // priceAtTick returns uint256 while tickAtPriceRatio expects int128
        // This test validates that the price calculation is reasonable
        assertGt(price, 0, "Price should be positive");
        assertLt(price, type(uint128).max, "Price should be within reasonable bounds");
    }

    // ========================================
    // FUZZ TESTS
    // ========================================

    function testFuzzTickAtPrice(uint256 tokenAmount, uint256 ethAmount) public {
        // Bound inputs to reasonable ranges to avoid overflow in ABDKMath64x64 conversions
        // int128 max is ~1.7e38, but we need to be more conservative for price ratios
        tokenAmount = bound(tokenAmount, 1, 1e18);
        ethAmount = bound(ethAmount, 1, 1e18);

        int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);

        // Tick should be within valid bounds
        assertGe(tick, TickMath.MIN_TICK, "Tick should be >= MIN_TICK");
        assertLe(tick, TickMath.MAX_TICK, "Tick should be <= MAX_TICK");
    }

    function testFuzzPriceAtTick(int24 tick) public {
        // Bound tick to reasonable range to avoid extreme prices
        // Further restrict to prevent overflow in price calculations
        tick = int24(bound(int256(tick), -200_000, 200_000));

        uint256 price = uniswapMath.priceAtTick(tick);

        // Price should be positive and within reasonable bounds
        assertGt(price, 0, "Price should be positive");
        assertLt(price, type(uint128).max, "Price should be within reasonable bounds");
    }

    function testFuzzClampToTickSpacing(int24 tick, int24 spacing) public {
        // Bound spacing to reasonable positive values
        spacing = int24(bound(int256(spacing), 1, 1000));

        int24 clampedTick = uniswapMath.clampToTickSpacing(tick, spacing);

        // Result should be within valid bounds
        assertGe(clampedTick, TickMath.MIN_TICK, "Clamped tick should be >= MIN_TICK");
        assertLe(clampedTick, TickMath.MAX_TICK, "Clamped tick should be <= MAX_TICK");

        // Result should be aligned to spacing (unless at boundaries)
        if (clampedTick != TickMath.MIN_TICK && clampedTick != TickMath.MAX_TICK) {
            assertEq(clampedTick % spacing, 0, "Clamped tick should be aligned to spacing");
        }
    }
}
