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

import {Test} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {
    AaveQuotePushFlashReceiver,
    IAaveExternalUnwinder
} from "../src/AaveQuotePushFlashReceiver.sol";
import {AtomicBridgeCoordinator} from "atomic/AtomicBridgeCoordinator.sol";
import {AtomicFeePolicy} from "atomic/AtomicFeePolicy.sol";
import {AtomicFulfillerRegistry} from "atomic/AtomicFulfillerRegistry.sol";
import {AtomicLiquidityVault} from "atomic/AtomicLiquidityVault.sol";
import {AtomicObligationEscrow} from "atomic/AtomicObligationEscrow.sol";
import {AtomicSettlementRouter} from "atomic/AtomicSettlementRouter.sol";
import {AtomicSlashingManager} from "atomic/AtomicSlashingManager.sol";
import {AtomicTypes} from "atomic/AtomicTypes.sol";
import {IAtomicSettlementAdapter} from "atomic/interfaces/IAtomicSettlementAdapter.sol";
import {MockMintableToken} from "repo-test/dbis/MockMintableToken.sol";

contract AaveForkMockExternalUnwinder is IAaveExternalUnwinder {
    IERC20 public immutable base;
    IERC20 public immutable quote;
    uint256 public immutable numerator;
    uint256 public immutable denominator;

    constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) {
        base = base_;
        quote = quote_;
        numerator = numerator_;
        denominator = denominator_;
    }

    function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata)
        external
        override
        returns (uint256 amountOut)
    {
        require(tokenIn == address(base), "base only");
        require(tokenOut == address(quote), "quote only");
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        amountOut = amountIn * numerator / denominator;
        require(amountOut >= minAmountOut, "min unwind");
        IERC20(address(quote)).transfer(msg.sender, amountOut);
    }
}

contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter {
    address public lastToken;
    uint256 public lastAmount;
    address public lastRecipient;
    bytes32 public lastObligationId;

    function executeSettlement(
        bytes32 obligationId,
        address token,
        uint256 amount,
        address recipient,
        bytes calldata
    ) external payable returns (bytes32 settlementId) {
        lastObligationId = obligationId;
        lastToken = token;
        lastAmount = amount;
        lastRecipient = recipient;
        settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, block.timestamp));
    }
}

contract AaveQuotePushFlashReceiverMainnetForkTest is Test {
    address constant AAVE_POOL_MAINNET = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
    address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
    address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
    address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
    bytes32 constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE");

    AaveQuotePushFlashReceiver internal receiver;
    AaveForkMockExternalUnwinder internal unwinder;
    MockMintableToken internal cusdc138;
    MockMintableToken internal bondToken;
    AtomicLiquidityVault internal atomicVault;
    AtomicFulfillerRegistry internal atomicRegistry;
    AtomicFeePolicy internal atomicFeePolicy;
    AtomicObligationEscrow internal atomicEscrow;
    AtomicSettlementRouter internal atomicRouter;
    AtomicSlashingManager internal atomicSlashingManager;
    AtomicBridgeCoordinator internal atomicCoordinator;
    MockAtomicSettlementAdapter internal mockSettlementAdapter;
    bytes32 internal atomicCorridorId;
    address internal destinationRecipient = address(0x138138);

    function setUp() public {
        string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
        vm.createSelectFork(rpcUrl);

        receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET, address(this));
        unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100);
        deal(USDC, address(unwinder), 50_000_000_000);

        cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this));
        bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this));
        atomicVault = new AtomicLiquidityVault(address(this));
        atomicRegistry = new AtomicFulfillerRegistry(address(bondToken), address(this));
        atomicFeePolicy = new AtomicFeePolicy(address(this));
        atomicEscrow = new AtomicObligationEscrow(address(this));
        atomicRouter = new AtomicSettlementRouter(address(this));
        atomicSlashingManager = new AtomicSlashingManager(address(atomicRegistry), address(this));
        mockSettlementAdapter = new MockAtomicSettlementAdapter();
        atomicCoordinator = new AtomicBridgeCoordinator(
            address(atomicVault),
            address(atomicRegistry),
            address(atomicEscrow),
            address(atomicRouter),
            address(atomicFeePolicy),
            address(atomicSlashingManager),
            address(this),
            address(this)
        );

        atomicVault.grantRole(atomicVault.COORDINATOR_ROLE(), address(atomicCoordinator));
        atomicVault.grantRole(atomicVault.RECONCILER_ROLE(), address(atomicCoordinator));
        atomicRegistry.grantRole(atomicRegistry.COORDINATOR_ROLE(), address(atomicCoordinator));
        atomicRegistry.grantRole(atomicRegistry.SLASHER_ROLE(), address(atomicSlashingManager));
        atomicEscrow.grantRole(atomicEscrow.COORDINATOR_ROLE(), address(atomicCoordinator));
        atomicRouter.grantRole(atomicRouter.COORDINATOR_ROLE(), address(atomicCoordinator));
        atomicSlashingManager.grantRole(atomicSlashingManager.COORDINATOR_ROLE(), address(atomicCoordinator));
        atomicRouter.setAdapter(MOCK_SETTLEMENT_MODE, address(mockSettlementAdapter));

        atomicCorridorId = atomicCoordinator.getCorridorId(1, 138, CWUSDC, address(cusdc138));
        atomicCoordinator.configureCorridor(
            AtomicTypes.CorridorConfig({
                enabled: true,
                degraded: false,
                sourceChain: 1,
                destinationChain: 138,
                assetIn: CWUSDC,
                assetOut: address(cusdc138),
                maxNotional: 10_000_000,
                maxReservedBps: 8_000,
                targetBuffer: 100_000,
                maxSettlementBacklog: 5_000_000,
                maxOracleDriftBps: 500,
                fulfilmentTimeout: 1 days,
                settlementTimeout: 2 days,
                defaultSettlementMode: MOCK_SETTLEMENT_MODE
            })
        );
        atomicFeePolicy.setCorridorPolicy(atomicCorridorId, 25, 10, 12_000, 500, 1 days, 2 days);
        atomicVault.setTargetBuffer(atomicCorridorId, address(cusdc138), 100_000);
        cusdc138.mint(address(this), 5_000_000);
        cusdc138.approve(address(atomicVault), type(uint256).max);
        atomicVault.fundCorridor(atomicCorridorId, address(cusdc138), 5_000_000);

        bondToken.mint(address(receiver), 5_000_000);
        vm.startPrank(address(receiver));
        bondToken.approve(address(atomicRegistry), type(uint256).max);
        atomicRegistry.depositBond(3_000_000);
        vm.stopPrank();
        atomicRegistry.setFulfillerActive(address(receiver), true);
        atomicRegistry.setCorridorAuthorization(address(receiver), atomicCorridorId, true);
    }

    function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public {
        uint256 amount = 2_964_298;
        uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
        uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);

        AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
            integration: DODO_PMM_INTEGRATION_MAINNET,
            pmmPool: POOL_CWUSDC_USDC,
            baseToken: CWUSDC,
            externalUnwinder: address(unwinder),
            minOutPmm: 2_800_000,
            minOutUnwind: amount + 1_483,
            unwindData: bytes(""),
            atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
                coordinator: address(0),
                sourceChain: 0,
                destinationChain: 0,
                destinationAsset: address(0),
                bridgeAmount: 0,
                minDestinationAmount: 0,
                destinationRecipient: address(0),
                destinationDeadline: 0,
                routeId: bytes32(0),
                settlementMode: bytes32(0),
                submitCommitment: false
            }),
            collateral: receiver.emptyCollateralParams()
        });

        receiver.flashQuotePush(USDC, amount, p);

        uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
        uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
        uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
        uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
        uint256 premium = _aavePremium(amount);
        (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
            _predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, 0, premium);
        uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);

        assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver retains surplus");
        assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "base fully unwound");
        assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased");
        assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased");
        _assertWithinOnePercent(actualBaseOut, predictedBaseOut, "baseOut");
        _assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "netQuoteIn");
        _assertWithinOnePercent(actualSurplus, predictedSurplus, "surplus");
        assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "unwindOut");
    }

    function testFork_aaveQuotePush_atomicCorridorFulfillment_1_to_138_cwusdc_to_cusdc() public {
        uint256 amount = 2_964_298;
        uint256 bridgeAmount = 500_000;
        uint256 destinationRecipientBefore = cusdc138.balanceOf(destinationRecipient);
        uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);

        AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
            integration: DODO_PMM_INTEGRATION_MAINNET,
            pmmPool: POOL_CWUSDC_USDC,
            baseToken: CWUSDC,
            externalUnwinder: address(unwinder),
            minOutPmm: 2_800_000,
            minOutUnwind: amount + 1_483,
            unwindData: bytes(""),
            atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
                coordinator: address(atomicCoordinator),
                sourceChain: 1,
                destinationChain: 138,
                destinationAsset: address(cusdc138),
                bridgeAmount: bridgeAmount,
                minDestinationAmount: bridgeAmount,
                destinationRecipient: destinationRecipient,
                destinationDeadline: block.timestamp + 1 hours,
                routeId: atomicCorridorId,
                settlementMode: bytes32(0),
                submitCommitment: true
            }),
            collateral: receiver.emptyCollateralParams()
        });

        uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
        receiver.flashQuotePush(USDC, amount, p);

        uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
        uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
        uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
        uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
        uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
        uint256 premium = _aavePremium(amount);
        (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
            _predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, bridgeAmount, premium);
        uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
        assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver still retains quote surplus");
        assertEq(cusdc138.balanceOf(destinationRecipient), destinationRecipientBefore + bridgeAmount, "destination funded");
        assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "remaining base fully unwound");
        _assertWithinOnePercent(actualBaseOut, predictedBaseOut, "atomic baseOut");
        _assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "atomic netQuoteIn");
        _assertWithinOnePercent(actualSurplus, predictedSurplus, "atomic surplus");
        assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "atomic unwindOut");

        AtomicTypes.CorridorLiquidityState memory state =
            atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
        assertEq(state.settlementBacklog, bridgeAmount, "backlog increased by delivered amount");
        assertEq(state.totalLiquidity, 5_000_000 - bridgeAmount, "vault liquidity debited on immediate fulfillment");
    }

    function testFork_aaveQuotePush_atomicCorridorSettlementConfirmation_1_to_138() public {
        uint256 amount = 2_964_298;
        uint256 bridgeAmount = 500_000;
        uint256 deadline = block.timestamp + 1 hours;
        uint256 receiverBondBefore = atomicRegistry.availableBond(address(receiver));
        uint256 treasuryBaseBefore = IERC20(CWUSDC).balanceOf(address(this));
        uint256 receiverBaseBefore = IERC20(CWUSDC).balanceOf(address(receiver));

        AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
            integration: DODO_PMM_INTEGRATION_MAINNET,
            pmmPool: POOL_CWUSDC_USDC,
            baseToken: CWUSDC,
            externalUnwinder: address(unwinder),
            minOutPmm: 2_800_000,
            minOutUnwind: amount + 1_483,
            unwindData: bytes(""),
            atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
                coordinator: address(atomicCoordinator),
                sourceChain: 1,
                destinationChain: 138,
                destinationAsset: address(cusdc138),
                bridgeAmount: bridgeAmount,
                minDestinationAmount: bridgeAmount,
                destinationRecipient: destinationRecipient,
                destinationDeadline: deadline,
                routeId: atomicCorridorId,
                settlementMode: bytes32(0),
                submitCommitment: true
            }),
            collateral: receiver.emptyCollateralParams()
        });

        receiver.flashQuotePush(USDC, amount, p);
        bytes32 obligationId = _deriveObligationId(bridgeAmount, deadline);

        AtomicTypes.AtomicObligation memory fulfilled = atomicCoordinator.getObligation(obligationId);
        assertEq(uint8(fulfilled.status), uint8(AtomicTypes.ObligationStatus.Fulfilled), "obligation fulfilled");

        atomicCoordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(138))));

        AtomicTypes.AtomicObligation memory pending = atomicCoordinator.getObligation(obligationId);
        assertEq(uint8(pending.status), uint8(AtomicTypes.ObligationStatus.SettlementPending), "obligation pending");
        assertEq(mockSettlementAdapter.lastObligationId(), obligationId, "adapter saw obligation");
        assertEq(mockSettlementAdapter.lastToken(), CWUSDC, "adapter token");
        assertEq(mockSettlementAdapter.lastRecipient(), destinationRecipient, "adapter recipient");

        uint256 expectedFulfillerFee = (bridgeAmount * 25) / 10_000;
        uint256 expectedProtocolFee = (bridgeAmount * 10) / 10_000;
        uint256 expectedSettlementAmount = bridgeAmount - expectedFulfillerFee - expectedProtocolFee;
        assertEq(mockSettlementAdapter.lastAmount(), expectedSettlementAmount, "adapter amount");
        assertEq(IERC20(CWUSDC).balanceOf(address(this)), treasuryBaseBefore + expectedProtocolFee, "protocol fee received");
        assertEq(
            IERC20(CWUSDC).balanceOf(address(receiver)),
            receiverBaseBefore + expectedFulfillerFee,
            "fulfiller fee received"
        );

        cusdc138.mint(address(this), bridgeAmount);
        cusdc138.approve(address(atomicVault), bridgeAmount);
        atomicCoordinator.confirmSettlement(obligationId, bridgeAmount);

        AtomicTypes.AtomicObligation memory settled = atomicCoordinator.getObligation(obligationId);
        assertEq(uint8(settled.status), uint8(AtomicTypes.ObligationStatus.Settled), "obligation settled");

        AtomicTypes.CorridorLiquidityState memory state =
            atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
        assertEq(state.settlementBacklog, 0, "backlog cleared");
        assertEq(state.totalLiquidity, 5_000_000, "vault replenished");
        assertEq(atomicRegistry.availableBond(address(receiver)), receiverBondBefore, "bond released");
    }

    function _deriveObligationId(uint256 bridgeAmount, uint256 deadline) internal view returns (bytes32) {
        bytes32 intentId = keccak256(
            abi.encode(
                block.chainid,
                address(receiver),
                uint256(1),
                atomicCorridorId,
                bridgeAmount,
                bridgeAmount,
                deadline,
                atomicCorridorId
            )
        );
        return keccak256(abi.encode(intentId, destinationRecipient));
    }

    function _predictQuotePush(
        uint256 baseReserve,
        uint256 quoteReserve,
        uint256 grossQuoteIn,
        uint256 lpFeeBps,
        uint256 unwindNumerator,
        uint256 unwindDenominator,
        uint256 bridgeAmount,
        uint256 premium
    ) internal pure returns (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) {
        uint256 netQuoteIn = (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
        predictedBaseOut = (netQuoteIn * baseReserve) / (quoteReserve + netQuoteIn);
        uint256 remainingBase = predictedBaseOut - bridgeAmount;
        predictedUnwindOut = (remainingBase * unwindNumerator) / unwindDenominator;
        predictedSurplus = predictedUnwindOut - grossQuoteIn - premium;
    }

    function _netQuoteIn(uint256 grossQuoteIn, uint256 lpFeeBps) internal pure returns (uint256) {
        return (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
    }

    function _assertWithinOnePercent(uint256 actual, uint256 expected, string memory label) internal pure {
        if (expected == 0) {
            require(actual == 0, label);
            return;
        }
        uint256 diff = actual > expected ? actual - expected : expected - actual;
        require(diff * 10_000 <= expected * 100, label);
    }

    function _aavePremium(uint256 amount) internal pure returns (uint256) {
        return (amount * 5) / 10_000;
    }
}
