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

import { V3Fixtures } from "../helpers/V3Fixtures.sol";
import { PartnershipBondV3Safe } from "../../contracts/bonds/PartnershipBondV3Safe.sol";
import { AccountabilityBondV3Safe } from "../../contracts/bonds/AccountabilityBondV3Safe.sol";
import { BondMathLibV3 } from "../../contracts/lib/BondMathLibV3.sol";
import { ObjectiveRuleSchemaV3 } from "../../contracts/bonds/ObjectiveRuleSchemaV3.sol";
import { ThresholdConsensusV3 } from "../../contracts/consensus/ThresholdConsensusV3.sol";
import { PullPaymentsV3 } from "../../contracts/bonds/PullPaymentsV3.sol";

contract ForcedNativeTransfer {
    constructor() payable { }

    function force(address target) external {
        selfdestruct(payable(target));
    }
}

/// @notice Native-only pilot-cap coverage. The values below are test fixtures,
/// not deployment recommendations; production values remain unset in the manifest.
contract BondNativeCapsV3Test is V3Fixtures {
    uint256 private constant PARTY_CAP = 5 ether;
    uint256 private constant PARTNERSHIP_BOND_CAP = 8 ether;
    uint256 private constant PARTNERSHIP_AGGREGATE_CAP = 10 ether;
    uint256 private constant ACCOUNTABILITY_BOND_CAP = 6 ether;
    uint256 private constant ACCOUNTABILITY_AGGREGATE_CAP = 10 ether;

    PartnershipBondV3Safe private partnership;
    AccountabilityBondV3Safe private accountability;

    function setUp() public {
        _setUpDependencies();
        partnership = new PartnershipBondV3Safe(
            owner,
            address(0),
            address(consensus),
            address(disputeRegistry),
            PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            address(0)
        );
        accountability = new AccountabilityBondV3Safe(
            owner,
            address(consensus),
            address(disputeRegistry),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0)
        );
    }

    function test_partnershipTermsRejectPerPartyAndPerBondCapPlusOne() external {
        PartnershipBondV3Safe.Terms memory overParty = _partnershipTerms(1, PARTY_CAP + 1, 1);
        bytes memory overPartySigA = _signPartnership(partnership, overParty, PARTY_A_KEY);
        bytes memory overPartySigB = _signPartnership(partnership, overParty, PARTY_B_KEY);
        vm.expectRevert(PartnershipBondV3Safe.PrincipalCapExceeded.selector);
        partnership.createBond(overParty, overPartySigA, overPartySigB);

        PartnershipBondV3Safe.Terms memory overBond =
            _partnershipTerms(2, PARTY_CAP, PARTNERSHIP_BOND_CAP - PARTY_CAP + 1);
        bytes memory overBondSigA = _signPartnership(partnership, overBond, PARTY_A_KEY);
        bytes memory overBondSigB = _signPartnership(partnership, overBond, PARTY_B_KEY);
        vm.expectRevert(PartnershipBondV3Safe.PrincipalCapExceeded.selector);
        partnership.createBond(overBond, overBondSigA, overBondSigB);

        PartnershipBondV3Safe.Terms memory exact = _partnershipTerms(3, PARTY_CAP, PARTNERSHIP_BOND_CAP - PARTY_CAP);
        assertEq(_createPartnership(partnership, exact), 1, "exact signed principal cap was not accepted");
    }

    function test_accountabilityTermsRejectPerBondCapPlusOne() external {
        AccountabilityBondV3Safe.Terms memory overBond = _accountabilityTerms(1, ACCOUNTABILITY_BOND_CAP + 1);
        bytes memory overBondSigCompany = _signAccountability(accountability, overBond, COMPANY_KEY);
        bytes memory overBondSigCounterparty = _signAccountability(accountability, overBond, COUNTERPARTY_KEY);
        vm.expectRevert(AccountabilityBondV3Safe.EnforcementRequired.selector);
        accountability.createBond(overBond, overBondSigCompany, overBondSigCounterparty);

        AccountabilityBondV3Safe.Terms memory exact = _accountabilityTerms(2, ACCOUNTABILITY_BOND_CAP);
        assertEq(_createAccountability(accountability, exact), 1, "exact signed principal cap was not accepted");
    }

    function test_partnershipAggregateSaturationPersistsUntilPausedRefundWithdrawals() external {
        uint256 first = _createPartnership(partnership, _terms(1, 5 ether, 3 ether));
        _fundPartnership(first, 5 ether, 3 ether);
        assertEq(partnership.nativeLiability(), 8 ether);

        uint256 second = _createPartnership(partnership, _terms(2, 2 ether, 2 ether));
        vm.deal(partyA, 2 ether);
        vm.prank(partyA);
        partnership.fundNative{ value: 2 ether }(second);
        assertEq(partnership.nativeLiability(), PARTNERSHIP_AGGREGATE_CAP);

        vm.deal(partyB, 2 ether);
        vm.prank(partyB);
        vm.expectRevert(PartnershipBondV3Safe.AggregateCapExceeded.selector);
        partnership.fundNative{ value: 2 ether }(second);

        vm.prank(partyA);
        partnership.cancelBeforeActivation(first);
        assertEq(partnership.nativeLiability(), PARTNERSHIP_AGGREGATE_CAP, "crediting a refund released capacity early");

        vm.prank(owner);
        partnership.pause();
        vm.prank(partyA);
        partnership.withdrawCredit(address(0));
        vm.prank(partyB);
        partnership.withdrawCredit(address(0));
        assertEq(partnership.nativeLiability(), 2 ether, "withdrawals did not release exactly refunded principal");

        vm.prank(owner);
        partnership.unpause();
        vm.deal(partyB, 2 ether);
        vm.prank(partyB);
        partnership.fundNative{ value: 2 ether }(second);
        assertEq(partnership.nativeLiability(), 4 ether, "released capacity was not reusable");
    }

    function test_legacySettlementPathIsFailClosed() external {
        uint256 bondId = _createPartnership(partnership, _terms(1, 5 ether, 3 ether));
        _fundActivePartnership(partnership, bondId, 5 ether, 3 ether);

        vm.prank(partyA);
        vm.expectRevert(PartnershipBondV3Safe.PilotMutualConsentOnly.selector);
        partnership.requestSettlement(bondId);
    }

    function test_accountabilityAggregateRefundReleaseAndForcedTransferIsolation() external {
        uint256 first = _createAccountability(accountability, _accountabilityTermsForBond(1, ACCOUNTABILITY_BOND_CAP));
        vm.deal(company, ACCOUNTABILITY_BOND_CAP);
        vm.prank(company);
        accountability.fundNative{ value: ACCOUNTABILITY_BOND_CAP }(first);

        vm.deal(address(this), 3 ether);
        ForcedNativeTransfer donation = new ForcedNativeTransfer{ value: 3 ether }();
        donation.force(address(accountability));
        assertEq(
            accountability.nativeLiability(), ACCOUNTABILITY_BOND_CAP, "forced transfer consumed liability capacity"
        );
        assertEq(address(accountability).balance, 9 ether, "forced transfer was not received");

        uint256 second = _createAccountability(accountability, _accountabilityTermsForBond(2, 5 ether));
        vm.deal(company, 5 ether);
        vm.prank(company);
        vm.expectRevert(AccountabilityBondV3Safe.AggregateCapExceeded.selector);
        accountability.fundNative{ value: 5 ether }(second);

        vm.prank(company);
        accountability.cancelBeforeActivation(first);
        assertEq(accountability.nativeLiability(), ACCOUNTABILITY_BOND_CAP, "refund credit released capacity early");

        vm.prank(owner);
        accountability.pause();
        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(accountability.nativeLiability(), 0, "paused refund withdrawal did not release capacity");

        vm.prank(owner);
        accountability.unpause();
        vm.deal(company, 5 ether);
        vm.prank(company);
        accountability.fundNative{ value: 5 ether }(second);
        assertEq(accountability.nativeLiability(), 5 ether);
    }

    function testFuzz_accountabilityCapacityTracksRefundedPrincipal(uint96 rawStake) external {
        uint256 stake = bound(uint256(rawStake), 1, ACCOUNTABILITY_BOND_CAP);
        uint256 id = _createAccountability(accountability, _accountabilityTermsForBond(1, stake));
        vm.deal(company, stake);
        vm.prank(company);
        accountability.fundNative{ value: stake }(id);
        assertEq(accountability.nativeLiability(), stake);

        vm.prank(counterparty);
        accountability.cancelBeforeActivation(id);
        assertEq(accountability.nativeLiability(), stake, "refund credit released capacity before withdrawal");
        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(accountability.nativeLiability(), 0, "withdrawal released an incorrect amount");
    }

    function testFuzz_cumulativePartialFundingConservesPrincipalAndCannotDoubleWithdraw(
        uint96 rawStake,
        uint96 rawFirstPart
    ) external {
        uint256 stake = bound(uint256(rawStake), 2, ACCOUNTABILITY_BOND_CAP);
        uint256 firstPart = bound(uint256(rawFirstPart), 1, stake - 1);
        uint256 id = _createAccountability(accountability, _accountabilityTermsForBond(1, stake));
        vm.deal(company, stake);
        vm.startPrank(company);
        accountability.fundNative{ value: firstPart }(id);
        accountability.fundNative{ value: stake - firstPart }(id);
        vm.deal(company, 1);
        vm.expectRevert(AccountabilityBondV3Safe.Overfund.selector);
        accountability.fundNative{ value: 1 }(id);
        accountability.cancelBeforeActivation(id);
        vm.stopPrank();

        assertEq(accountability.creditOf(address(0), company), stake);
        assertEq(accountability.nativeLiability(), stake);
        uint256 before = company.balance;
        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(company.balance - before, stake);
        assertEq(accountability.nativeLiability(), 0);
        assertEq(address(accountability).balance, 0);

        vm.expectRevert(PullPaymentsV3.NoCredit.selector);
        vm.prank(company);
        accountability.withdrawCredit(address(0));
    }

    function _terms(uint96 nonce, uint256 stakeA, uint256 stakeB)
        private
        view
        returns (PartnershipBondV3Safe.Terms memory t)
    {
        t = _partnershipTerms(nonce, stakeA, stakeB);
        t.bondId = partnership.nextBondId();
    }

    function _fundPartnership(uint256 bondId, uint256 stakeA, uint256 stakeB) private {
        vm.deal(partyA, stakeA);
        vm.prank(partyA);
        partnership.fundNative{ value: stakeA }(bondId);
        vm.deal(partyB, stakeB);
        vm.prank(partyB);
        partnership.fundNative{ value: stakeB }(bondId);
    }

    function _accountabilityTermsForBond(uint96 nonce, uint256 stake)
        private
        view
        returns (AccountabilityBondV3Safe.Terms memory t)
    {
        t = _accountabilityTerms(nonce, stake);
        t.bondId = accountability.nextBondId();
    }

    function _partnershipClaim(
        PartnershipBondV3Safe bonds,
        uint256 bondId,
        bytes32 schema,
        bytes32 snapshot,
        uint96 nonce
    ) private view returns (ThresholdConsensusV3.Claim memory) {
        return ThresholdConsensusV3.Claim({
            schema: schema,
            subject: bonds.disputeSubject(bondId),
            nonce: nonce,
            expiresAt: uint64(block.timestamp + 10 days),
            settlementSnapshot: snapshot,
            outcome: bonds.SETTLEMENT_OUTCOME()
        });
    }
}
