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

import {CommonBase} from "../lib/forge-std/src/Base.sol";
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol";
import {StdChains} from "../lib/forge-std/src/StdChains.sol";
import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol";
import {StdUtils} from "../lib/forge-std/src/StdUtils.sol";
import {Test} from "../lib/forge-std/src/Test.sol";
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Deploy} from "./Deploy.sol";
import {IPartyPool} from "../src/IPartyPool.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {MockERC20} from "./PartyPlanner.t.sol";

// Mock ERC20 token for testing
contract MockERC20 is ERC20 {
    uint8 private _decimals;

    constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
        _decimals = decimals_;
    }

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

    function decimals() public view override returns (uint8) {
        return _decimals;
    }
}

contract PartyPlannerTest is Test {
    PartyPlanner public planner;
    MockERC20 public tokenA;
    MockERC20 public tokenB;
    MockERC20 public tokenC;

    address public payer = makeAddr("payer");
    address public receiver = makeAddr("receiver");

    uint256 constant INITIAL_MINT_AMOUNT = 1000000e18;
    uint256 constant INITIAL_DEPOSIT_AMOUNT = 1000e18;

    function setUp() public {
        // Deploy PartyPlanner owned by this test contract
        planner = Deploy.newPartyPlanner(address(this));

        // Deploy mock _tokens
        tokenA = new MockERC20("Token A", "TKNA", 18);
        tokenB = new MockERC20("Token B", "TKNB", 18);
        tokenC = new MockERC20("Token C", "TKNC", 6);

        // Mint _tokens to payer
        tokenA.mint(payer, INITIAL_MINT_AMOUNT);
        tokenB.mint(payer, INITIAL_MINT_AMOUNT);
        tokenC.mint(payer, INITIAL_MINT_AMOUNT);

        // Approve _tokens for PartyPlanner
        vm.startPrank(payer);
        tokenA.approve(address(planner), type(uint256).max);
        tokenB.approve(address(planner), type(uint256).max);
        tokenC.approve(address(planner), type(uint256).max);
        vm.stopPrank();
    }

    function test_createPool_Success() public {
        // Prepare pool parameters
        string memory name = "Test Pool";
        string memory symbol = "TESTLP";
        IERC20[] memory tokens = new IERC20[](2);
        tokens[0] = IERC20(address(tokenA));
        tokens[1] = IERC20(address(tokenB));

        uint256[] memory bases = new uint256[](2);
        bases[0] = 1e18; // 18 decimals
        bases[1] = 1e18; // 18 decimals

        uint256[] memory initialDeposits = new uint256[](2);
        initialDeposits[0] = INITIAL_DEPOSIT_AMOUNT;
        initialDeposits[1] = INITIAL_DEPOSIT_AMOUNT;

        // Fixed point parameters (using simple values for testing)
        int128 tradeFrac = int128((1 << 64) - 1); // slightly less than 1.0 in 64.64 fixed point
        int128 targetSlippage = int128(1 << 62); // 0.25 in 64.64 fixed point
        uint256 swapFeePpm = 3000; // 0.3%
        uint256 flashFeePpm = 5000; // 0.5%

        uint256 initialPoolCount = planner.poolCount();
        uint256 initialTokenACount = planner.poolsByTokenCount(IERC20(address(tokenA)));
        uint256 initialTokenBCount = planner.poolsByTokenCount(IERC20(address(tokenB)));

        // Compute kappa then create pool via kappa overload
        int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);

        (IPartyPool pool, uint256 lpAmount) = planner.newPool(
            name,
            symbol,
            tokens,
            bases,
            computedKappa,
            swapFeePpm,
            flashFeePpm,
            false, // not stable
            payer,
            receiver,
            initialDeposits,
            1000e18, // initial LP amount
            0 // no deadline
        );

        // Verify pool was created
        assertNotEq(address(pool), address(0), "Pool should be created");
        assertGt(lpAmount, 0, "LP tokens should be minted");

        // Verify pool is indexed correctly
        assertEq(planner.poolCount(), initialPoolCount + 1, "Pool count should increase by 1");
        assertTrue(planner.getPoolSupported(address(pool)), "Pool should be marked as supported");

        // Verify token indexing
        assertEq(planner.poolsByTokenCount(IERC20(address(tokenA))), initialTokenACount + 1, "TokenA pool count should increase");
        assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase");

        // Verify pools can be retrieved
        IPartyPool[] memory allPools = planner.getAllPools(0, 10);
        bool poolFound = false;
        for (uint256 i = 0; i < allPools.length; i++) {
            if (allPools[i] == pool) {
                poolFound = true;
                break;
            }
        }
        assertTrue(poolFound, "Created pool should be in getAllPools result");

        // Verify pool appears in token-specific queries
        IPartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10);
        bool poolInTokenA = false;
        for (uint256 i = 0; i < tokenAPools.length; i++) {
            if (tokenAPools[i] == pool) {
                poolInTokenA = true;
                break;
            }
        }
        assertTrue(poolInTokenA, "Pool should be indexed under tokenA");

        IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
        bool poolInTokenB = false;
        for (uint256 i = 0; i < tokenBPools.length; i++) {
            if (tokenBPools[i] == pool) {
                poolInTokenB = true;
                break;
            }
        }
        assertTrue(poolInTokenB, "Pool should be indexed under tokenB");

        // Verify LP _tokens were minted to receiver
        assertEq(pool.balanceOf(receiver), lpAmount, "Receiver should have LP tokens");
    }

    function test_createPool_MultiplePoolsIndexing() public {
        // Create first pool with tokenA and tokenB
        IERC20[] memory tokens1 = new IERC20[](2);
        tokens1[0] = IERC20(address(tokenA));
        tokens1[1] = IERC20(address(tokenB));

        uint256[] memory bases1 = new uint256[](2);
        bases1[0] = 1e18;
        bases1[1] = 1e18;

        uint256[] memory deposits1 = new uint256[](2);
        deposits1[0] = INITIAL_DEPOSIT_AMOUNT;
        deposits1[1] = INITIAL_DEPOSIT_AMOUNT;

        int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
        (IPartyPool pool1,) = planner.newPool(
            "Pool 1", "LP1", tokens1, bases1,
            kappa1, 3000, 5000, false,
            payer, receiver, deposits1, 1000e18, 0
        );

        // Create second pool with tokenB and tokenC
        IERC20[] memory tokens2 = new IERC20[](2);
        tokens2[0] = IERC20(address(tokenB));
        tokens2[1] = IERC20(address(tokenC));

        uint256[] memory bases2 = new uint256[](2);
        bases2[0] = 1e18;
        bases2[1] = 1e6; // tokenC has 6 decimals

        uint256[] memory deposits2 = new uint256[](2);
        deposits2[0] = INITIAL_DEPOSIT_AMOUNT;
        deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals

        int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
        (IPartyPool pool2,) = planner.newPool(
            "Pool 2", "LP2", tokens2, bases2,
            kappa2, 3000, 5000, false,
            payer, receiver, deposits2, 1000e18, 0
        );

        // Verify indexing
        assertEq(planner.poolCount(), 2, "Should have 2 pools");
        assertEq(planner.tokenCount(), 3, "Should have 3 unique tokens");

        // Verify token-pool relationships
        assertEq(planner.poolsByTokenCount(IERC20(address(tokenA))), 1, "TokenA should be in 1 pool");
        assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), 2, "TokenB should be in 2 pools");
        assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool");

        // Verify tokenB appears in both pools
        IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
        assertEq(tokenBPools.length, 2, "TokenB should have 2 pools");

        bool pool1Found = false;
        bool pool2Found = false;
        for (uint256 i = 0; i < tokenBPools.length; i++) {
            if (tokenBPools[i] == pool1) pool1Found = true;
            if (tokenBPools[i] == pool2) pool2Found = true;
        }
        assertTrue(pool1Found, "Pool1 should be in tokenB pools");
        assertTrue(pool2Found, "Pool2 should be in tokenB pools");
    }

    function test_createPool_InvalidInputs() public {
        IERC20[] memory tokens = new IERC20[](2);
        tokens[0] = IERC20(address(tokenA));
        tokens[1] = IERC20(address(tokenB));

        uint256[] memory bases = new uint256[](2);
        bases[0] = 1e18;
        bases[1] = 1e18;

        uint256[] memory deposits = new uint256[](1); // Mismatched length
        deposits[0] = INITIAL_DEPOSIT_AMOUNT;

        // Test token/deposit length mismatch
        vm.expectRevert("Planner: tokens and deposits length mismatch");
        // call old-signature convenience (it will still exist) for the mismatched-length revert check
        planner.newPool(
            "Test Pool", "TESTLP", tokens, bases,
            int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
            payer, receiver, deposits, 1000e18, 0
        );

        // Test zero payer address
        uint256[] memory validDeposits = new uint256[](2);
        validDeposits[0] = INITIAL_DEPOSIT_AMOUNT;
        validDeposits[1] = INITIAL_DEPOSIT_AMOUNT;

        int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));

        vm.expectRevert("Planner: payer cannot be zero address");
        planner.newPool(
            "Test Pool", "TESTLP", tokens, bases,
            kappaErr, 3000, 5000, false,
            address(0), receiver, validDeposits, 1000e18, 0
        );

        // Test zero receiver address
        vm.expectRevert("Planner: receiver cannot be zero address");
        planner.newPool(
            "Test Pool", "TESTLP", tokens, bases,
            kappaErr, 3000, 5000, false,
            payer, address(0), validDeposits, 1000e18, 0
        );

        // Test deadline exceeded
        // The default timestamp is 1 and 1-0 is 0 which means "ignore deadline," so we need to set a proper timestamp.
        int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
        vm.warp(1000);
        vm.expectRevert("Planner: deadline exceeded");
        planner.newPool(
            "Test Pool", "TESTLP", tokens, bases,
            kappaDeadline, 3000, 5000, false,
            payer, receiver, validDeposits, 1000e18, block.timestamp - 1
        );
    }

    function test_poolIndexing_Pagination() public {
        // Create multiple pools for pagination testing
        uint256 numPools = 5;
        IPartyPool[] memory createdPools = new IPartyPool[](numPools);

        for (uint256 i = 0; i < numPools; i++) {
            IERC20[] memory tokens = new IERC20[](2);
            tokens[0] = IERC20(address(tokenA));
            tokens[1] = IERC20(address(tokenB));

            uint256[] memory bases = new uint256[](2);
            bases[0] = 1e18;
            bases[1] = 1e18;

            uint256[] memory deposits = new uint256[](2);
            deposits[0] = INITIAL_DEPOSIT_AMOUNT;
            deposits[1] = INITIAL_DEPOSIT_AMOUNT;

            int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
            (IPartyPool pool,) = planner.newPool(
                string(abi.encodePacked("Pool ", vm.toString(i))),
                string(abi.encodePacked("LP", vm.toString(i))),
                tokens, bases,
                kappaLoop, 3000, 5000, false,
                payer, receiver, deposits, 1000e18, 0
            );

            createdPools[i] = pool;
        }

        assertEq(planner.poolCount(), numPools, "Should have created all pools");

        // Test pagination - get first 3 pools
        IPartyPool[] memory page1 = planner.getAllPools(0, 3);
        assertEq(page1.length, 3, "First page should have 3 pools");

        // Test pagination - get next 2 pools
        IPartyPool[] memory page2 = planner.getAllPools(3, 3);
        assertEq(page2.length, 2, "Second page should have 2 pools");

        // Test pagination - offset beyond bounds
        IPartyPool[] memory emptyPage = planner.getAllPools(10, 3);
        assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset");

        // Verify all pools are accessible through pagination
        IPartyPool[] memory allPools = planner.getAllPools(0, 10);
        assertEq(allPools.length, numPools, "Should return all pools");

        for (uint256 i = 0; i < numPools; i++) {
            assertEq(address(allPools[i]), address(createdPools[i]), "Pool order should be preserved");
        }
    }
}
