// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { Test } from "forge-std/Test.sol";
import { PullPaymentsV3 } from "../../contracts/bonds/PullPaymentsV3.sol";
import { TaskEscrowV3 } from "../../contracts/escrow/TaskEscrowV3.sol";
import { FeeOnTransferMockERC20 } from "../mocks/FeeOnTransferMockERC20.sol";
import { MockERC20 } from "../mocks/MockERC20.sol";
import { ForceNativeDonation, NoDeliveryMockERC20, OutboundFeeMockERC20 } from "../mocks/MaliciousERC20s.sol";

/// @notice Asset accounting and all TaskEscrow terminal paths, with fuzzed
/// exact-amount cases. Every terminal transition must preserve solvency.
contract TaskEscrowV3AssetAccountingTest is Test {
    address internal constant OWNER = address(0xBEEF);
    address internal constant TREASURY = address(0xCAFE);
    address internal constant CREATOR = address(0xC0FFEE);
    address internal constant BIDDER = address(0xB1D);
    address internal constant OTHER = address(0x0A11CE);

    uint256 internal constant TASK_CAP = 100 ether;
    uint256 internal constant AGGREGATE_CAP = 150 ether;
    uint64 internal constant DECISION_TIMEOUT = 2 days;

    TaskEscrowV3 internal escrow;
    MockERC20 internal token;

    function setUp() public {
        escrow = new TaskEscrowV3(OWNER, TREASURY, 500, TASK_CAP, AGGREGATE_CAP, DECISION_TIMEOUT);
        token = new MockERC20();
    }

    function testFuzz_erc20TaskExactReceiptCapsAndPullSettlement(uint96 rawAmount) external {
        uint256 amount = bound(uint256(rawAmount), 2, TASK_CAP);
        uint256 id = _createTokenTask(token, amount);

        assertEq(token.balanceOf(address(escrow)), amount);
        assertEq(escrow.assets(address(token)), amount);
        assertEq(escrow.liabilities(address(token)), amount);
        assertTrue(escrow.isSolvent(address(token)));
        assertEq(escrow.surplus(address(token)), 0);

        vm.prank(BIDDER);
        escrow.placeBid(id);
        vm.prank(CREATOR);
        escrow.acceptBid(id, BIDDER);
        vm.prank(BIDDER);
        escrow.submitWork(id);
        vm.prank(CREATOR);
        escrow.approve(id);

        uint256 fee = amount * 500 / 10_000;
        uint256 pay = amount - fee;
        assertEq(escrow.creditOf(address(token), BIDDER), pay);
        assertEq(escrow.creditOf(address(token), TREASURY), fee);
        assertEq(escrow.creditLiabilities(address(token)), amount);
        assertEq(escrow.liabilities(address(token)), amount);

        vm.prank(BIDDER);
        escrow.withdrawCredit(address(token));
        assertEq(escrow.liabilities(address(token)), fee);
        assertEq(token.balanceOf(address(escrow)), fee);
        assertTrue(escrow.isSolvent(address(token)));

        if (fee != 0) {
            vm.prank(TREASURY);
            escrow.withdrawCredit(address(token));
        }
        assertEq(escrow.liabilities(address(token)), 0);
        assertEq(token.balanceOf(address(escrow)), 0);
    }

    function test_maxTaskAmountFeeCalculationDoesNotOverflowOrOrphanPrincipal() external {
        TaskEscrowV3 maxEscrow =
            new TaskEscrowV3(OWNER, TREASURY, 500, type(uint256).max, type(uint256).max, DECISION_TIMEOUT);
        uint256 amount = type(uint256).max;

        vm.deal(CREATOR, amount);
        vm.prank(CREATOR);
        uint256 id =
            maxEscrow.createTaskNative{ value: amount }(_assigneeDeadline(), _submitDeadline(), _reviewDeadline());

        vm.prank(BIDDER);
        maxEscrow.placeBid(id);
        vm.prank(CREATOR);
        maxEscrow.acceptBid(id, BIDDER);
        vm.prank(BIDDER);
        maxEscrow.submitWork(id);
        vm.prank(CREATOR);
        maxEscrow.approve(id);

        uint256 fee = amount / 20; // 500 / 10_000, rounded down.
        uint256 pay = amount - fee;
        assertEq(maxEscrow.creditOf(address(0), BIDDER), pay, "assignee credit");
        assertEq(maxEscrow.creditOf(address(0), TREASURY), fee, "treasury credit");
        assertEq(maxEscrow.liabilities(address(0)), amount, "principal was not fully allocated");
        assertTrue(maxEscrow.isSolvent(address(0)), "max-value settlement lost backing");
    }

    function test_taskSnapshotsFeeAndTreasuryAtFunding() external {
        address laterTreasury = address(0xFEE);
        vm.deal(CREATOR, 1 ether);
        vm.prank(CREATOR);
        uint256 id =
            escrow.createTaskNative{ value: 1 ether }(_assigneeDeadline(), _submitDeadline(), _reviewDeadline());
        vm.prank(OWNER);
        escrow.setProtocolFee(0);
        vm.prank(OWNER);
        escrow.setTreasury(laterTreasury);

        vm.prank(BIDDER);
        escrow.placeBid(id);
        vm.prank(CREATOR);
        escrow.acceptBid(id, BIDDER);
        vm.prank(BIDDER);
        escrow.submitWork(id);
        vm.prank(CREATOR);
        escrow.approve(id);

        assertEq(escrow.creditOf(address(0), TREASURY), 0.05 ether);
        assertEq(escrow.creditOf(address(0), laterTreasury), 0);
        assertEq(escrow.creditOf(address(0), BIDDER), 0.95 ether);
    }

    function test_erc20CreationRejectsNoDeliveryAndFeeOnTransfer() external {
        NoDeliveryMockERC20 noDelivery = new NoDeliveryMockERC20();
        noDelivery.mint(CREATOR, 10 ether);
        vm.startPrank(CREATOR);
        noDelivery.approve(address(escrow), type(uint256).max);
        vm.expectRevert(PullPaymentsV3.UnsupportedAssetTransfer.selector);
        escrow.createTaskERC20(address(noDelivery), 10 ether, _assigneeDeadline(), _submitDeadline(), _reviewDeadline());
        vm.stopPrank();

        FeeOnTransferMockERC20 fee = new FeeOnTransferMockERC20();
        fee.mint(CREATOR, 10 ether);
        vm.startPrank(CREATOR);
        fee.approve(address(escrow), type(uint256).max);
        vm.expectRevert(PullPaymentsV3.UnsupportedAssetTransfer.selector);
        escrow.createTaskERC20(address(fee), 10 ether, _assigneeDeadline(), _submitDeadline(), _reviewDeadline());
        vm.stopPrank();
        assertEq(escrow.liabilities(address(noDelivery)), 0);
        assertEq(escrow.liabilities(address(fee)), 0);
    }

    function test_outboundFeeTokenCannotConsumePullCreditWithoutExactDelivery() external {
        OutboundFeeMockERC20 fee = new OutboundFeeMockERC20();
        uint256 id = _createTokenTask(fee, 10 ether);
        _submit(id);
        vm.prank(CREATOR);
        escrow.approve(id);

        vm.prank(BIDDER);
        vm.expectRevert(PullPaymentsV3.UnsupportedAssetTransfer.selector);
        escrow.withdrawCredit(address(fee));
        assertGt(escrow.creditOf(address(fee), BIDDER), 0);
        assertEq(escrow.liabilities(address(fee)), 10 ether);
        assertTrue(escrow.isSolvent(address(fee)));
    }

    function test_duplicateBiddersCreatorBidAndUnlistedAcceptanceAreRejected() external {
        uint256 id = _createNativeTask(1 ether);
        vm.prank(CREATOR);
        vm.expectRevert(TaskEscrowV3.InvalidBidder.selector);
        escrow.placeBid(id);

        vm.prank(BIDDER);
        escrow.placeBid(id);
        vm.prank(BIDDER);
        vm.expectRevert(TaskEscrowV3.DuplicateBidder.selector);
        escrow.placeBid(id);

        vm.prank(CREATOR);
        vm.expectRevert(TaskEscrowV3.InvalidBidder.selector);
        escrow.acceptBid(id, OTHER);
        vm.prank(CREATOR);
        escrow.acceptBid(id, BIDDER);
    }

    function testFuzz_immutablePerTaskAndPerAssetAggregateCaps(uint96 rawFirst) external {
        uint256 first = bound(uint256(rawFirst), 1, TASK_CAP);
        _createNativeTask(first);
        assertEq(escrow.maxTaskAmount(), TASK_CAP);
        assertEq(escrow.maxAggregateLiabilityPerAsset(), AGGREGATE_CAP);

        vm.deal(CREATOR, TASK_CAP + 1);
        vm.prank(CREATOR);
        vm.expectRevert(TaskEscrowV3.TaskCapExceeded.selector);
        escrow.createTaskNative{ value: TASK_CAP + 1 }(_assigneeDeadline(), _submitDeadline(), _reviewDeadline());

        uint256 remaining = AGGREGATE_CAP - first;
        uint256 firstFiller = remaining > TASK_CAP ? TASK_CAP : remaining;
        if (firstFiller != 0) _createNativeTask(firstFiller);
        remaining = AGGREGATE_CAP - escrow.liabilities(address(0));
        if (remaining != 0) _createNativeTask(remaining);

        vm.deal(CREATOR, 1);
        vm.prank(CREATOR);
        vm.expectRevert(TaskEscrowV3.AggregateCapExceeded.selector);
        escrow.createTaskNative{ value: 1 }(_assigneeDeadline(), _submitDeadline(), _reviewDeadline());
    }

    function test_openAndAssignedDeadlineRefundsArePermissionless() external {
        uint256 openId = _createNativeTask(1 ether);
        vm.warp(_assigneeDeadline());
        vm.prank(OTHER);
        escrow.forceRefund(openId);
        assertEq(escrow.creditOf(address(0), CREATOR), 1 ether);

        uint256 assignedId = _createNativeTask(2 ether);
        vm.prank(BIDDER);
        escrow.placeBid(assignedId);
        vm.prank(CREATOR);
        escrow.acceptBid(assignedId, BIDDER);
        vm.warp(_submitDeadline());
        vm.prank(OTHER);
        escrow.forceRefund(assignedId);
        assertEq(escrow.creditOf(address(0), CREATOR), 3 ether);
        assertEq(escrow.liabilities(address(0)), 3 ether);
    }

    function test_submittedAndDisputedDecisionTimeoutDefaultsArePermissionless() external {
        uint256 submittedId = _createNativeTask(1 ether);
        _submit(submittedId);
        vm.warp(_reviewDeadline());
        vm.prank(OTHER);
        escrow.defaultApproveSubmitted(submittedId);
        assertEq(escrow.creditOf(address(0), BIDDER), 0.95 ether);
        assertEq(escrow.creditOf(address(0), TREASURY), 0.05 ether);

        uint256 disputedId = _createNativeTask(2 ether);
        _submit(disputedId);
        vm.prank(CREATOR);
        escrow.dispute(disputedId, keccak256("evidence"));
        (,,,,,,, uint64 decisionDeadline,,,,,) = escrow.tasks(disputedId);
        vm.warp(decisionDeadline);
        vm.prank(OWNER);
        vm.expectRevert(TaskEscrowV3.DeadlinePassed.selector);
        escrow.resolveDispute(disputedId, 1 ether);
        vm.prank(OTHER);
        escrow.defaultResolveDispute(disputedId);
        assertEq(escrow.creditOf(address(0), CREATOR), 2 ether);
        assertEq(escrow.liabilities(address(0)), 3 ether);
    }

    function test_forcedDonationIsSurplusAndOwnerCannotDrainTaskObligations() external {
        _createNativeTask(1 ether);
        vm.prank(OWNER);
        vm.expectRevert(TaskEscrowV3.InsufficientSurplus.selector);
        escrow.skimSurplus(address(0), OWNER, 1);

        ForceNativeDonation donor = new ForceNativeDonation{ value: 2 ether }();
        donor.force(payable(address(escrow)));
        assertEq(escrow.assets(address(0)), 3 ether);
        assertEq(escrow.liabilities(address(0)), 1 ether);
        assertEq(escrow.surplus(address(0)), 2 ether);

        uint256 ownerBefore = OWNER.balance;
        vm.prank(OWNER);
        escrow.skimSurplus(address(0), OWNER, 2 ether);
        assertEq(OWNER.balance, ownerBefore + 2 ether);
        assertEq(escrow.assets(address(0)), 1 ether);
        assertEq(escrow.liabilities(address(0)), 1 ether);
        assertTrue(escrow.isSolvent(address(0)));
    }

    function _createNativeTask(uint256 amount) private returns (uint256 id) {
        vm.deal(CREATOR, amount);
        vm.prank(CREATOR);
        id = escrow.createTaskNative{ value: amount }(_assigneeDeadline(), _submitDeadline(), _reviewDeadline());
    }

    function _createTokenTask(MockERC20 asset, uint256 amount) private returns (uint256 id) {
        asset.mint(CREATOR, amount);
        vm.startPrank(CREATOR);
        asset.approve(address(escrow), type(uint256).max);
        id = escrow.createTaskERC20(address(asset), amount, _assigneeDeadline(), _submitDeadline(), _reviewDeadline());
        vm.stopPrank();
    }

    function _createTokenTask(OutboundFeeMockERC20 asset, uint256 amount) private returns (uint256 id) {
        asset.mint(CREATOR, amount);
        vm.startPrank(CREATOR);
        asset.approve(address(escrow), type(uint256).max);
        id = escrow.createTaskERC20(address(asset), amount, _assigneeDeadline(), _submitDeadline(), _reviewDeadline());
        vm.stopPrank();
    }

    function _submit(uint256 id) private {
        vm.prank(BIDDER);
        escrow.placeBid(id);
        vm.prank(CREATOR);
        escrow.acceptBid(id, BIDDER);
        vm.prank(BIDDER);
        escrow.submitWork(id);
    }

    function _assigneeDeadline() private view returns (uint64) {
        return uint64(block.timestamp + 1 days);
    }

    function _submitDeadline() private view returns (uint64) {
        return uint64(block.timestamp + 2 days);
    }

    function _reviewDeadline() private view returns (uint64) {
        return uint64(block.timestamp + 3 days);
    }
}
