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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Test} from "forge-std/Test.sol";
import {DBISEngineXSingleSidedDodoCwusdcVault} from
    "../../contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol";
import {MockMintableToken} from "../dbis/MockMintableToken.sol";

contract MockDodoPool {
    using SafeERC20 for IERC20;

    address public immutable base;
    address public immutable quote;
    uint256 public baseReserve;
    uint256 public quoteReserve;

    constructor(address base_, address quote_) {
        base = base_;
        quote = quote_;
    }

    function _BASE_TOKEN_() external view returns (address) {
        return base;
    }

    function _QUOTE_TOKEN_() external view returns (address) {
        return quote;
    }

    function querySellBase(address, uint256 payBaseAmount) external pure returns (uint256 receiveQuoteAmount, uint256 mtFee) {
        return (payBaseAmount, 0);
    }

    function querySellQuote(address, uint256 payQuoteAmount) external pure returns (uint256 receiveBaseAmount, uint256 mtFee) {
        return (payQuoteAmount, 0);
    }

    function getVaultReserve() external view returns (uint256, uint256) {
        return (baseReserve, quoteReserve);
    }

    function buyShares(address) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) {
        uint256 baseBalance = IERC20(base).balanceOf(address(this));
        uint256 quoteBalance = IERC20(quote).balanceOf(address(this));
        baseShare = baseBalance - baseReserve;
        quoteShare = quoteBalance - quoteReserve;
        baseReserve = baseBalance;
        quoteReserve = quoteBalance;
        lpShare = baseShare < quoteShare ? baseShare : quoteShare;
    }
}

contract MockDodoIntegration {
    using SafeERC20 for IERC20;

    function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount)
        external
        returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare)
    {
        require(baseAmount > 0 && quoteAmount > 0, "zero amount");
        address base = MockDodoPool(pool)._BASE_TOKEN_();
        address quote = MockDodoPool(pool)._QUOTE_TOKEN_();
        IERC20(base).safeTransferFrom(msg.sender, pool, baseAmount);
        IERC20(quote).safeTransferFrom(msg.sender, pool, quoteAmount);
        return MockDodoPool(pool).buyShares(msg.sender);
    }
}

contract DBISEngineXSingleSidedDodoCwusdcVaultTest is Test {
    MockMintableToken internal cwusdc;
    MockMintableToken internal weth;
    MockDodoIntegration internal integration;
    MockDodoPool internal pool;
    DBISEngineXSingleSidedDodoCwusdcVault internal vault;

    address internal constant FUNDER = address(0xF00D);
    address internal constant OWNER = address(0xA11CE);

    function setUp() public {
        cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this));
        weth = new MockMintableToken("Wrapped Ether", "WETH", 18, address(this));
        integration = new MockDodoIntegration();
        pool = new MockDodoPool(address(cwusdc), address(weth));
        vault = new DBISEngineXSingleSidedDodoCwusdcVault(address(cwusdc), address(weth), address(integration), OWNER);

        cwusdc.mint(FUNDER, 100_000_000);
        weth.mint(FUNDER, 1 ether);
        vm.startPrank(FUNDER);
        cwusdc.approve(address(vault), type(uint256).max);
        weth.approve(address(vault), type(uint256).max);
        vm.stopPrank();
    }

    function testAcceptsSingleSidedCwusdcAsInventoryButNotExecutable() public {
        vm.prank(FUNDER);
        vault.depositCwusdc(10_000_000);

        (
            uint256 cwusdcBalance,
            uint256 quoteBalance,
            uint256 cwusdcInventory,
            uint256 quoteInventory,
            bool solvent,
            bool executable
        ) = vault.solvencyState();

        assertEq(cwusdcBalance, 10_000_000, "cw balance");
        assertEq(quoteBalance, 0, "quote balance");
        assertEq(cwusdcInventory, 10_000_000, "cw inventory");
        assertEq(quoteInventory, 0, "quote inventory");
        assertTrue(solvent, "single-sided inventory is solvent");
        assertFalse(executable, "single-sided inventory is not executable DODO liquidity");
    }

    function testPromoteRequiresTwoSidedInventory() public {
        vm.prank(OWNER);
        vault.setDodoPool(address(pool));
        vm.prank(OWNER);
        vault.setCanary(1_000, 1_000, 1_000, 1_000);

        vm.prank(FUNDER);
        vault.depositCwusdc(10_000_000);

        vm.expectRevert(bytes("two-sided required"));
        vm.prank(OWNER);
        vault.promoteToDodo(1_000_000, 0, 0, 0, 0);

        vm.expectRevert(bytes("insufficient quote inventory"));
        vm.prank(OWNER);
        vault.promoteToDodo(1_000_000, 1, 0, 0, 0);
    }

    function testPromotesTwoSidedInventoryAndPassesCanary() public {
        vm.prank(OWNER);
        vault.setDodoPool(address(pool));
        vm.prank(OWNER);
        vault.setCanary(1_000, 1_000, 1_000, 1_000);

        vm.startPrank(FUNDER);
        vault.depositCwusdc(10_000_000);
        vault.depositQuote(1 ether);
        vm.stopPrank();

        vm.prank(OWNER);
        (uint256 baseShare, uint256 quoteShare, uint256 lpShare) =
            vault.promoteToDodo(2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000);

        assertEq(baseShare, 2_000_000, "base share");
        assertEq(quoteShare, 2_000_000, "quote share");
        assertEq(lpShare, 2_000_000, "lp share");
        assertEq(vault.accountedCwusdcInventory(), 8_000_000, "remaining cw inventory");
        assertEq(vault.accountedQuoteInventory(), 1 ether - 2_000_000, "remaining quote inventory");
        assertEq(cwusdc.balanceOf(address(pool)), 2_000_000, "pool cw balance");
        assertEq(weth.balanceOf(address(pool)), 2_000_000, "pool quote balance");
        assertTrue(vault.canaryPasses(), "canary passes");
    }

    function testCannotRescueAccountedInventory() public {
        vm.prank(FUNDER);
        vault.depositCwusdc(10_000_000);

        vm.expectRevert(bytes("cwusdc insolvent"));
        vm.prank(OWNER);
        vault.rescueUnaccountedToken(address(cwusdc), OWNER, 1);
    }

    function testOwnerCanWithdrawAccountedInventory() public {
        vm.prank(FUNDER);
        vault.depositCwusdc(10_000_000);

        vm.prank(OWNER);
        vault.withdrawCwusdcInventory(OWNER, 4_000_000);

        assertEq(vault.accountedCwusdcInventory(), 6_000_000, "inventory decremented");
        assertEq(cwusdc.balanceOf(OWNER), 4_000_000, "owner received");
    }
}
