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

import { V3Fixtures } from "../helpers/V3Fixtures.sol";
import { AccountabilityBondV3Safe } from "../../contracts/bonds/AccountabilityBondV3Safe.sol";
import { BondStateV3 } from "../../contracts/bonds/BondStateV3.sol";
import { PartnershipBondV3Safe } from "../../contracts/bonds/PartnershipBondV3Safe.sol";

/// @notice Regression coverage for the signed funding and active-period windows.
/// @dev Both windows are half-open: a deadline is the first instant at which
///      the applicable permissionless expiry path is available.
contract BondLifecycleDeadlineBoundsV3Test is V3Fixtures {
    PartnershipBondV3Safe private partnership;
    AccountabilityBondV3Safe private accountability;

    event BondCreatedV3(uint256 indexed bondId, address indexed partyA, address indexed partyB, bytes32 termsHash);

    function setUp() public {
        _setUpDependencies();
        partnership = new PartnershipBondV3Safe(
            owner, address(0), address(consensus), address(disputeRegistry), 10 ether, 20 ether, 30 ether, address(0)
        );
        accountability = new AccountabilityBondV3Safe(
            owner, address(consensus), address(disputeRegistry), 20 ether, 30 ether, address(0)
        );
    }

    function test_partnershipFundingClosesAtProposalDeadlineAndExpiryReturnsPartialPrincipal() external {
        PartnershipBondV3Safe.Terms memory terms = _partnershipTerms(1, 2 ether, 1 ether);
        uint256 bondId = _createPartnership(partnership, terms);

        vm.warp(terms.proposalDeadline - 1);
        vm.deal(partyA, 2 ether);
        vm.prank(partyA);
        partnership.fundNative{ value: 1 ether }(bondId);
        assertEq(partnership.nativeLiability(), 1 ether, "partial funding did not occupy liability capacity");

        vm.warp(terms.proposalDeadline);
        vm.prank(partyA);
        vm.expectRevert(PartnershipBondV3Safe.DeadlinePassed.selector);
        partnership.fundNative{ value: 1 ether }(bondId);

        partnership.expireUnfunded(bondId);
        PartnershipBondV3Safe.BondData memory b = partnership.bondOf(bondId);
        assertEq(uint256(b.state), uint256(BondStateV3.State.ForceExited), "deadline did not force-exit partial bond");
        assertEq(partnership.creditOf(address(0), partyA), 1 ether, "partial principal was not credited");
        assertEq(partnership.nativeLiability(), 1 ether, "credit released capacity before withdrawal");

        vm.prank(partyA);
        partnership.withdrawCredit(address(0));
        assertEq(partnership.creditOf(address(0), partyA), 0, "withdrawal left a stuck credit");
        assertEq(partnership.nativeLiability(), 0, "withdrawal did not release capacity");
    }

    function test_partnershipFundingFailsAfterProposalDeadline() external {
        PartnershipBondV3Safe.Terms memory terms = _partnershipTerms(1, 1 ether, 1 ether);
        uint256 bondId = _createPartnership(partnership, terms);

        vm.warp(terms.proposalDeadline + 1);
        vm.deal(partyA, 1 ether);
        vm.prank(partyA);
        vm.expectRevert(PartnershipBondV3Safe.DeadlinePassed.selector);
        partnership.fundNative{ value: 1 ether }(bondId);
        assertEq(partnership.nativeLiability(), 0, "reverted late funding occupied capacity");
    }

    function test_accountabilityFundingClosesAtActivePeriodStartAndExpiryReturnsPartialPrincipal() external {
        AccountabilityBondV3Safe.Terms memory terms = _accountabilityTerms(1, 2 ether);
        uint256 bondId = _createAccountability(accountability, terms);

        vm.warp(terms.activePeriodStart - 1);
        vm.deal(company, 2 ether);
        vm.prank(company);
        accountability.fundNative{ value: 1 ether }(bondId);
        assertEq(accountability.nativeLiability(), 1 ether, "partial funding did not occupy liability capacity");

        vm.warp(terms.activePeriodStart);
        vm.prank(company);
        vm.expectRevert(AccountabilityBondV3Safe.DeadlinePassed.selector);
        accountability.fundNative{ value: 1 ether }(bondId);

        accountability.expireUnfunded(bondId);
        AccountabilityBondV3Safe.BondData memory b = accountability.bondOf(bondId);
        assertEq(uint256(b.state), uint256(BondStateV3.State.ForceExited), "deadline did not force-exit partial bond");
        assertEq(accountability.creditOf(address(0), company), 1 ether, "partial principal was not credited");
        assertEq(accountability.nativeLiability(), 1 ether, "credit released capacity before withdrawal");

        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(accountability.creditOf(address(0), company), 0, "withdrawal left a stuck credit");
        assertEq(accountability.nativeLiability(), 0, "withdrawal did not release capacity");
    }

    function test_accountabilityFundingFailsAfterActivePeriodStart() external {
        AccountabilityBondV3Safe.Terms memory terms = _accountabilityTerms(1, 1 ether);
        uint256 bondId = _createAccountability(accountability, terms);

        vm.warp(terms.activePeriodStart + 1);
        vm.deal(company, 1 ether);
        vm.prank(company);
        vm.expectRevert(AccountabilityBondV3Safe.DeadlinePassed.selector);
        accountability.fundNative{ value: 1 ether }(bondId);
        assertEq(accountability.nativeLiability(), 0, "reverted late funding occupied capacity");
    }

    function test_partnershipActivationIsOpenOnlyBeforeActivePeriodEndAndExpiredFundsExit() external {
        PartnershipBondV3Safe.Terms memory first = _partnershipTerms(1, 1 ether, 1 ether);
        first.activePeriodEnd = uint64(block.timestamp + 4 days);
        uint256 firstId = _createPartnership(partnership, first);
        _fundPartnership(firstId, 1 ether, 1 ether);

        PartnershipBondV3Safe.Terms memory exact = _partnershipTerms(2, 1 ether, 1 ether);
        exact.bondId = partnership.nextBondId();
        exact.activePeriodEnd = first.activePeriodEnd;
        uint256 exactId = _createPartnership(partnership, exact);
        _fundPartnership(exactId, 1 ether, 1 ether);

        PartnershipBondV3Safe.Terms memory afterDeadline = _partnershipTerms(3, 1 ether, 1 ether);
        afterDeadline.bondId = partnership.nextBondId();
        afterDeadline.activePeriodEnd = first.activePeriodEnd;
        uint256 afterId = _createPartnership(partnership, afterDeadline);
        _fundPartnership(afterId, 1 ether, 1 ether);

        vm.warp(first.activePeriodEnd - 1);
        partnership.activate(firstId);
        assertEq(
            uint256(partnership.bondOf(firstId).state), uint256(BondStateV3.State.Active), "deadline-1 did not activate"
        );

        vm.warp(first.activePeriodEnd);
        vm.expectRevert(PartnershipBondV3Safe.DeadlinePassed.selector);
        partnership.activate(exactId);

        vm.warp(first.activePeriodEnd + 1);
        vm.expectRevert(PartnershipBondV3Safe.DeadlinePassed.selector);
        partnership.activate(afterId);

        partnership.expireUnactivated(exactId);
        partnership.expireUnactivated(afterId);
        partnership.markExpired(firstId);
        partnership.forceExit(firstId);
        assertEq(partnership.nativeLiability(), 6 ether, "terminal paths released capacity before withdrawals");

        vm.prank(partyA);
        partnership.withdrawCredit(address(0));
        vm.prank(partyB);
        partnership.withdrawCredit(address(0));
        assertEq(partnership.nativeLiability(), 0, "cancellation and expiry left stuck native principal");
    }

    function test_accountabilityActivationIsOpenOnlyBeforeActivePeriodEndAndExpiredFundsExit() external {
        AccountabilityBondV3Safe.Terms memory first = _accountabilityTerms(1, 1 ether);
        first.activePeriodEnd = uint64(block.timestamp + 10 days);
        uint256 firstId = _createAccountability(accountability, first);
        _fundAccountability(firstId, 1 ether);

        AccountabilityBondV3Safe.Terms memory exact = _accountabilityTerms(2, 1 ether);
        exact.bondId = accountability.nextBondId();
        exact.activePeriodEnd = first.activePeriodEnd;
        uint256 exactId = _createAccountability(accountability, exact);
        _fundAccountability(exactId, 1 ether);

        AccountabilityBondV3Safe.Terms memory afterDeadline = _accountabilityTerms(3, 1 ether);
        afterDeadline.bondId = accountability.nextBondId();
        afterDeadline.activePeriodEnd = first.activePeriodEnd;
        uint256 afterId = _createAccountability(accountability, afterDeadline);
        _fundAccountability(afterId, 1 ether);

        vm.warp(first.activePeriodEnd - 1);
        accountability.activate(firstId);
        assertEq(
            uint256(accountability.bondOf(firstId).state),
            uint256(BondStateV3.State.Active),
            "deadline-1 did not activate"
        );

        vm.warp(first.activePeriodEnd);
        vm.expectRevert(AccountabilityBondV3Safe.DeadlinePassed.selector);
        accountability.activate(exactId);

        vm.warp(first.activePeriodEnd + 1);
        vm.expectRevert(AccountabilityBondV3Safe.DeadlinePassed.selector);
        accountability.activate(afterId);

        accountability.expireUnactivated(exactId);
        accountability.expireUnactivated(afterId);
        accountability.markExpired(firstId);
        accountability.forceExit(firstId);
        assertEq(accountability.nativeLiability(), 3 ether, "terminal paths released capacity before withdrawals");

        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(accountability.nativeLiability(), 0, "cancellation and expiry left stuck native principal");
    }

    function test_partnershipSettlementCannotRaceExpiryAtActivePeriodEnd() external {
        PartnershipBondV3Safe.Terms memory terms = _partnershipTerms(1, 1 ether, 1 ether);
        uint256 bondId = _createPartnership(partnership, terms);
        _fundPartnership(bondId, 1 ether, 1 ether);
        vm.warp(terms.activePeriodStart);
        partnership.activate(bondId);

        vm.warp(terms.activePeriodEnd);
        vm.prank(partyA);
        vm.expectRevert(PartnershipBondV3Safe.PilotMutualConsentOnly.selector);
        partnership.requestSettlement(bondId);

        partnership.markExpired(bondId);
        partnership.forceExit(bondId);
        assertEq(partnership.creditOf(address(0), partyA), 1 ether);
        assertEq(partnership.creditOf(address(0), partyB), 1 ether);
    }

    function test_accountabilityBreachCannotRaceExpiryAtActivePeriodEnd() external {
        AccountabilityBondV3Safe.Terms memory terms = _accountabilityTerms(1, 1 ether);
        uint256 bondId = _createAccountability(accountability, terms);
        _fundAccountability(bondId, 1 ether);
        vm.warp(terms.activePeriodStart);
        accountability.activate(bondId);

        vm.warp(terms.activePeriodEnd);
        (bool lateOk,) = address(accountability)
            .call(
                abi.encodeWithSignature(
                    "attestBreach(uint256,uint256,bytes32,uint96)", bondId, 1 ether, keccak256("late breach"), 1
                )
            );
        assertFalse(lateOk);

        accountability.markExpired(bondId);
        accountability.forceExit(bondId);
        assertEq(accountability.creditOf(address(0), company), 1 ether);
        assertEq(accountability.creditOf(address(0), counterparty), 0);
    }

    function test_accountabilityRejectsZeroSlashAndMissingEvidence() external {
        AccountabilityBondV3Safe.Terms memory terms = _accountabilityTerms(1, 1 ether);
        uint256 bondId = _createAccountability(accountability, terms);
        _fundAccountability(bondId, 1 ether);
        vm.warp(terms.activePeriodStart);
        accountability.activate(bondId);

        (bool zeroOk,) = address(accountability)
            .call(
                abi.encodeWithSignature(
                    "attestBreach(uint256,uint256,bytes32,uint96)", bondId, 0, keccak256("breach"), 1
                )
            );
        (bool missingOk,) = address(accountability)
            .call(
                abi.encodeWithSignature("attestBreach(uint256,uint256,bytes32,uint96)", bondId, 1 ether, bytes32(0), 2)
            );
        assertFalse(zeroOk || missingOk);
    }

    function test_bondCreationEventsEmitStoredTermsHash() external {
        PartnershipBondV3Safe.Terms memory pTerms = _partnershipTerms(1, 1 ether, 1 ether);
        bytes32 pHash = partnership.hashTerms(pTerms);
        vm.expectEmit(true, true, true, true, address(partnership));
        emit BondCreatedV3(pTerms.bondId, pTerms.partyA, pTerms.partyB, pHash);
        uint256 partnershipId = _createPartnership(partnership, pTerms);
        assertEq(partnership.bondOf(partnershipId).termsHash, pHash);

        AccountabilityBondV3Safe.Terms memory aTerms = _accountabilityTerms(1, 1 ether);
        AccountabilityBondV3Safe.SignerConfig memory signers = _accountabilitySigners();
        AccountabilityBondV3Safe.EnforcementConfig memory enforcement = _accountabilityEnforcement(aTerms, signers);
        aTerms.objectiveBreachRulesHash = accountability.hashEnforcementConfig(enforcement);
        bytes32 aHash = accountability.hashTerms(aTerms);
        vm.expectEmit(true, true, true, true, address(accountability));
        emit BondCreatedV3(aTerms.bondId, aTerms.aiCompany, aTerms.counterparty, aHash);
        uint256 accountabilityId = _createAccountability(accountability, aTerms);
        assertEq(accountability.bondOf(accountabilityId).termsHash, aHash);
    }

    function testFuzz_partnershipExpiredPartialFundingReturnsAllPrincipal(uint96 rawAmount) external {
        uint256 amount = bound(uint256(rawAmount), 1, 1 ether - 1);
        PartnershipBondV3Safe.Terms memory terms = _partnershipTerms(1, 1 ether, 1 ether);
        uint256 bondId = _createPartnership(partnership, terms);

        vm.deal(partyA, amount);
        vm.prank(partyA);
        partnership.fundNative{ value: amount }(bondId);
        vm.warp(terms.proposalDeadline);
        partnership.expireUnfunded(bondId);
        assertEq(partnership.creditOf(address(0), partyA), amount, "expiry credit mismatched partial principal");
        assertEq(partnership.nativeLiability(), amount, "partial liability did not remain occupied");

        vm.prank(partyA);
        partnership.withdrawCredit(address(0));
        assertEq(partnership.nativeLiability(), 0, "partial withdrawal did not release capacity");
    }

    function testFuzz_accountabilityExpiredPartialFundingReturnsAllPrincipal(uint96 rawAmount) external {
        uint256 amount = bound(uint256(rawAmount), 1, 1 ether - 1);
        AccountabilityBondV3Safe.Terms memory terms = _accountabilityTerms(1, 1 ether);
        uint256 bondId = _createAccountability(accountability, terms);

        vm.deal(company, amount);
        vm.prank(company);
        accountability.fundNative{ value: amount }(bondId);
        vm.warp(terms.activePeriodStart);
        accountability.expireUnfunded(bondId);
        assertEq(accountability.creditOf(address(0), company), amount, "expiry credit mismatched partial principal");
        assertEq(accountability.nativeLiability(), amount, "partial liability did not remain occupied");

        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(accountability.nativeLiability(), 0, "partial withdrawal did not release capacity");
    }

    function test_permissionlessFundedNeverActivatedRefundsWorkWhilePaused() external {
        PartnershipBondV3Safe.Terms memory pTerms = _partnershipTerms(1, 1 ether, 1 ether);
        AccountabilityBondV3Safe.Terms memory aTerms = _accountabilityTerms(1, 1 ether);
        uint256 pId = _createPartnership(partnership, pTerms);
        uint256 aId = _createAccountability(accountability, aTerms);
        _fundPartnership(pId, 1 ether, 1 ether);
        _fundAccountability(aId, 1 ether);

        vm.prank(owner);
        partnership.pause();
        vm.prank(owner);
        accountability.pause();
        vm.warp(pTerms.activePeriodEnd);

        address keeper = address(0xB0B);
        vm.prank(keeper);
        partnership.expireUnactivated(pId);
        vm.prank(keeper);
        accountability.expireUnactivated(aId);

        assertEq(partnership.creditOf(address(0), partyA), 1 ether);
        assertEq(partnership.creditOf(address(0), partyB), 1 ether);
        assertEq(accountability.creditOf(address(0), company), 1 ether);

        vm.prank(partyA);
        partnership.withdrawCredit(address(0));
        vm.prank(partyB);
        partnership.withdrawCredit(address(0));
        vm.prank(company);
        accountability.withdrawCredit(address(0));
        assertEq(partnership.nativeLiability(), 0);
        assertEq(accountability.nativeLiability(), 0);
    }

    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 _fundAccountability(uint256 bondId, uint256 stake) private {
        vm.deal(company, stake);
        vm.prank(company);
        accountability.fundNative{ value: stake }(bondId);
    }
}
