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

import { PartnershipBondV3Safe } from "../../contracts/bonds/PartnershipBondV3Safe.sol";
import { AccountabilityBondV3Safe } from "../../contracts/bonds/AccountabilityBondV3Safe.sol";
import { RewardProgramV3 } from "../../contracts/bonds/RewardProgramV3.sol";
import { TaskEscrowV3 } from "../../contracts/escrow/TaskEscrowV3.sol";
import { DisputeRegistryV3 } from "../../contracts/disputes/DisputeRegistryV3.sol";
import { ERC8004ValidationRegistryV3Safe } from "../../contracts/reputation/ERC8004ValidationRegistryV3Safe.sol";
import { MutualAidPoolV3 } from "../../contracts/mutualaid/MutualAidPoolV3.sol";
import { VaultfireBIPVaultV3 } from "../../contracts/vault/VaultfireBIPVaultV3.sol";
import { MockERC20, V3Fixtures } from "../helpers/V3Fixtures.sol";

/// @title SolvencyAccountingProperties
/// @notice Deterministic and fuzzed unit properties for individual V3 accounting views.
/// @dev This intentionally is not a Foundry invariant target. The prior version let
///      Foundry invoke every deployed contract directly, producing mostly irrelevant
///      reverts. StatefulBondSafety.invariant_assetsCoverLiabilitiesAndNoOrphanedPrincipal
///      is the stateful solvency proof for the Safe bonds.
contract SolvencyAccountingProperties is V3Fixtures {
    PartnershipBondV3Safe internal partnership;
    AccountabilityBondV3Safe internal accountability;
    RewardProgramV3 internal rewards;
    TaskEscrowV3 internal escrow;
    DisputeRegistryV3 internal disputes;
    ERC8004ValidationRegistryV3Safe internal validators;
    MutualAidPoolV3 internal mutualAid;
    VaultfireBIPVaultV3 internal vault;
    MockERC20 internal token;

    address internal sponsor = address(0x5A0B50);
    address internal creator = address(0xC0FFEE);
    address internal validator = address(0xAA11D);
    uint256 internal partnershipBondId;
    uint256 internal accountabilityBondId;

    function setUp() public {
        _setUpDependencies();
        disputes = new DisputeRegistryV3(owner);
        vm.prank(owner);
        disputes.setAdjudicator(owner, true);
        vm.prank(owner);
        disputes.setAdjudicatorThreshold(1);
        assertEq(disputes.requiredAdjudicatorThreshold(), 1, "dispute threshold initialization failed");
        partnership = new PartnershipBondV3Safe(
            owner, address(0), address(consensus), address(disputes), 10 ether, 20 ether, 100 ether, address(0)
        );
        accountability =
            new AccountabilityBondV3Safe(owner, address(consensus), address(disputes), 10 ether, 100 ether, address(0));
        rewards = new RewardProgramV3(owner);
        escrow = new TaskEscrowV3(owner, address(0xCAFE), 0, 100 ether, 1_000 ether, 1 days);
        validators = new ERC8004ValidationRegistryV3Safe(owner, address(0xCAFE), 1 ether);
        mutualAid = new MutualAidPoolV3(owner, address(0), 10 ether, 10 ether);
        token = new MockERC20();
        vault = new VaultfireBIPVaultV3(address(token), owner, type(uint256).max);

        partnershipBondId = _createPartnership(partnership, _partnershipTerms(101, 2 ether, 3 ether));
        _fundActivePartnership(partnership, partnershipBondId, 2 ether, 3 ether);
        accountabilityBondId = _createAccountability(accountability, _accountabilityTerms(102, 4 ether));
        _fundActiveAccountability(accountability, accountabilityBondId, 4 ether);
        _seedRewardReserves();
        _seedTaskDisputeValidatorAndAidLiabilities();
        _seedVaultAssets();
    }

    function test_seededAccountingViewsCoverEveryRecordedLiability() external view {
        PartnershipBondV3Safe.BondData memory p = partnership.bondOf(partnershipBondId);
        assertEq(partnership.nativeLiability(), p.partyAFunded + p.partyBFunded);
        assertEq(
            address(partnership).balance, partnership.nativeLiability() + partnership.creditLiabilities(address(0))
        );
        assertTrue(partnership.isCreditSolvent(address(0)));

        AccountabilityBondV3Safe.BondData memory a = accountability.bondOf(accountabilityBondId);
        assertEq(address(accountability).balance, a.fundedAmount + accountability.creditLiabilities(address(0)));
        assertTrue(accountability.isCreditSolvent(address(0)));

        assertGe(rewards.assets(address(0)), rewards.liabilities(address(0)));
        assertGe(rewards.assets(address(token)), rewards.liabilities(address(token)));
        assertTrue(rewards.isSolvent(address(0)));
        assertTrue(rewards.isSolvent(address(token)));

        assertEq(escrow.assets(address(0)), escrow.liabilities(address(0)));
        assertTrue(escrow.isSolvent(address(0)));
        assertEq(address(disputes).balance, 0, "V3Safe bonds do not open custody disputes");
        assertTrue(disputes.isCreditSolvent(address(0)));

        (address stakeAsset, uint256 stakedAmount,,,) = validators.validators(validator);
        assertEq(stakeAsset, address(0));
        assertEq(
            address(validators).balance, stakedAmount + validators.withdrawableSlashedStake(address(0), address(0xCAFE))
        );
        assertEq(mutualAid.assets(), mutualAid.liabilities());
        assertTrue(mutualAid.isSolvent());
        assertGe(vault.totalAssets(), vault.convertToAssets(vault.totalShares()));
    }

    function testFuzz_validatorSlashRemainsFullyBacked(uint96 rawSlash) external {
        (, uint256 stakeBefore,,,) = validators.validators(validator);
        uint256 slash = bound(uint256(rawSlash), 0, stakeBefore);
        vm.prank(owner);
        validators.slash(validator, slash, keccak256("fuzz slash"));

        (, uint256 stakeAfter,,,) = validators.validators(validator);
        uint256 treasuryCredit = validators.withdrawableSlashedStake(address(0), address(0xCAFE));
        assertEq(stakeAfter + treasuryCredit, stakeBefore);
        assertEq(address(validators).balance, stakeAfter + treasuryCredit);
    }

    function testFuzz_nativeMutualAidContributionRemainsSolvent(uint96 rawContribution) external {
        uint256 contribution = bound(uint256(rawContribution), 1, 10 ether);
        vm.deal(sponsor, contribution);
        vm.prank(sponsor);
        (bool ok,) = payable(address(mutualAid)).call{ value: contribution }("");
        assertTrue(ok);
        assertEq(mutualAid.assets(), mutualAid.liabilities());
        assertTrue(mutualAid.isSolvent());
    }

    function _seedRewardReserves() internal {
        vm.deal(sponsor, 1 ether);
        vm.prank(sponsor);
        uint256 nativeProgram = rewards.createProgram(
            address(0),
            1 ether,
            uint64(block.timestamp - 1),
            uint64(block.timestamp + 7 days),
            bytes32(0),
            bytes32(0),
            bytes32(0),
            bytes32(0)
        );
        vm.prank(sponsor);
        rewards.fundNative{ value: 1 ether }(nativeProgram);

        token.mint(sponsor, 100 ether);
        vm.prank(sponsor);
        uint256 tokenProgram = rewards.createProgram(
            address(token),
            100 ether,
            uint64(block.timestamp - 1),
            uint64(block.timestamp + 7 days),
            bytes32(0),
            bytes32(0),
            bytes32(0),
            bytes32(0)
        );
        vm.startPrank(sponsor);
        token.approve(address(rewards), 100 ether);
        rewards.fundERC20(tokenProgram, 100 ether);
        vm.stopPrank();
    }

    function _seedTaskDisputeValidatorAndAidLiabilities() internal {
        vm.deal(creator, 2 ether);
        vm.prank(creator);
        escrow.createTaskNative{ value: 2 ether }(
            uint64(block.timestamp + 1 days), uint64(block.timestamp + 2 days), uint64(block.timestamp + 3 days)
        );

        vm.deal(validator, 1 ether);
        vm.prank(validator);
        validators.registerNative{ value: 1 ether }(block.timestamp + 7 days);

        vm.deal(sponsor, 2 ether);
        vm.prank(sponsor);
        (bool contributed,) = payable(address(mutualAid)).call{ value: 2 ether }("");
        assertTrue(contributed, "mutual aid funding failed");
    }

    function _seedVaultAssets() internal {
        token.mint(sponsor, 50 ether);
        vm.startPrank(sponsor);
        token.approve(address(vault), 50 ether);
        vault.deposit(50 ether, sponsor);
        vm.stopPrank();
    }
}
