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

import "forge-std/Test.sol";
import { PartnershipBondV3Safe } from "../../contracts/bonds/PartnershipBondV3Safe.sol";
import { AccountabilityBondV3Safe } from "../../contracts/bonds/AccountabilityBondV3Safe.sol";
import { AccountabilityCanonicalSourceV3 } from "../../contracts/bonds/AccountabilityCanonicalSourceV3.sol";
import { ThresholdConsensusV3 } from "../../contracts/consensus/ThresholdConsensusV3.sol";
import { DisputeRegistryV3 } from "../../contracts/disputes/DisputeRegistryV3.sol";

/// @dev Minimal local ERC-20 used only by V3 contract tests.
contract MockERC20 {
    string public constant name = "Test Token";
    string public constant symbol = "TEST";
    uint8 public constant decimals = 18;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    function mint(address to, uint256 amount) external {
        balanceOf[to] += amount;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        require(allowed >= amount, "allowance");
        allowance[from][msg.sender] = allowed - amount;
        _transfer(from, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) private {
        require(balanceOf[from] >= amount, "balance");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
    }
}

/// @dev Shared local deployment/signing fixture. All signatures are generated
///      by Foundry cheatcodes and are never keys for a live account.
abstract contract V3Fixtures is Test {
    uint256 internal constant PARTY_A_KEY = 0xA11CE;
    uint256 internal constant PARTY_B_KEY = 0xB0B;
    uint256 internal constant COMPANY_KEY = 0xC0A1;
    uint256 internal constant COUNTERPARTY_KEY = 0xC0A2;
    uint256 internal constant ATTESTER_1_KEY = 0xA771;
    uint256 internal constant ATTESTER_2_KEY = 0xA772;
    uint256 internal constant ADJUDICATOR_1_KEY = 0xAD01;
    uint256 internal constant ADJUDICATOR_2_KEY = 0xAD02;

    address internal partyA = vm.addr(PARTY_A_KEY);
    address internal partyB = vm.addr(PARTY_B_KEY);
    address internal company = vm.addr(COMPANY_KEY);
    address internal counterparty = vm.addr(COUNTERPARTY_KEY);
    address internal attester1 = vm.addr(ATTESTER_1_KEY);
    address internal attester2 = vm.addr(ATTESTER_2_KEY);
    address internal adjudicator1 = vm.addr(ADJUDICATOR_1_KEY);
    address internal adjudicator2 = vm.addr(ADJUDICATOR_2_KEY);
    address internal owner = address(0xBEEF);
    address internal secondAttester = address(0xA77E57);
    ThresholdConsensusV3 internal consensus;
    DisputeRegistryV3 internal disputeRegistry;
    AccountabilityCanonicalSourceV3 internal canonicalSource;

    function _setUpDependencies() internal {
        consensus = new ThresholdConsensusV3(owner, 2);
        vm.prank(owner);
        consensus.setAttester(owner, true);
        vm.prank(owner);
        consensus.setAttester(secondAttester, true);
        disputeRegistry = new DisputeRegistryV3(owner);
        canonicalSource = new AccountabilityCanonicalSourceV3();
        vm.prank(owner);
        disputeRegistry.setAdjudicator(owner, true);
        vm.prank(owner);
        disputeRegistry.setAdjudicatorThreshold(1);
    }

    event BondSettledV3(uint256 indexed bondId, uint256 partyAAmount, uint256 partyBAmount, uint256 partnershipAmount);

    function _partnershipTerms(uint96 nonce, uint256 partyAStake, uint256 partyBStake)
        internal
        view
        returns (PartnershipBondV3Safe.Terms memory t)
    {
        t = PartnershipBondV3Safe.Terms({
            bondId: 1,
            partyA: partyA,
            partyB: partyB,
            partyAStake: partyAStake,
            partyBStake: partyBStake,
            stakeAsset: address(0),
            partnershipType: bytes32("test"),
            breachConditionsHash: bytes32(0),
            objectiveRuleClass: 0,
            objectiveRuleSchemaVersion: 0,
            flourishRewardConditionsHash: bytes32(0),
            rewardProgramId: 0,
            proposalDeadline: uint64(block.timestamp + 1 days),
            activePeriodStart: uint64(block.timestamp + 2 days),
            activePeriodEnd: uint64(block.timestamp + 30 days),
            challengeWindow: uint64(3 days),
            nonce: nonce,
            chainId: block.chainid,
            protocolVersion: "3.0.0"
        });
    }

    function _accountabilityTerms(uint96 nonce, uint256 stake)
        internal
        view
        returns (AccountabilityBondV3Safe.Terms memory t)
    {
        t = AccountabilityBondV3Safe.Terms({
            bondId: 1,
            aiCompany: company,
            counterparty: counterparty,
            companyStake: stake,
            stakeAsset: address(0),
            missionHash: bytes32(0),
            objectiveBreachRulesHash: bytes32(0),
            objectiveRuleClass: 1,
            objectiveRuleSchemaVersion: 1,
            flourishRewardRulesHash: bytes32(0),
            rewardProgramId: 0,
            activePeriodStart: uint64(block.timestamp + 1 days),
            activePeriodEnd: uint64(block.timestamp + 30 days),
            challengeWindow: uint64(3 days),
            nonce: nonce,
            chainId: block.chainid,
            protocolVersion: "3.0.0"
        });
    }

    function _signPartnership(PartnershipBondV3Safe bonds, PartnershipBondV3Safe.Terms memory t, uint256 privateKey)
        internal
        returns (bytes memory)
    {
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", bonds.DOMAIN_SEPARATOR(), bonds.hashTerms(t)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
        return abi.encodePacked(r, s, v);
    }

    function _signAccountability(
        AccountabilityBondV3Safe bonds,
        AccountabilityBondV3Safe.Terms memory t,
        uint256 privateKey
    ) internal returns (bytes memory) {
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", bonds.DOMAIN_SEPARATOR(), bonds.hashTerms(t)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
        return abi.encodePacked(r, s, v);
    }

    function _createPartnership(PartnershipBondV3Safe bonds, PartnershipBondV3Safe.Terms memory t)
        internal
        returns (uint256)
    {
        return bonds.createBond(t, _signPartnership(bonds, t, PARTY_A_KEY), _signPartnership(bonds, t, PARTY_B_KEY));
    }

    function _createAccountability(AccountabilityBondV3Safe bonds, AccountabilityBondV3Safe.Terms memory t)
        internal
        returns (uint256)
    {
        AccountabilityBondV3Safe.SignerConfig memory signers = _accountabilitySigners();
        AccountabilityBondV3Safe.EnforcementConfig memory enforcement = _accountabilityEnforcement(t, signers);
        t.objectiveBreachRulesHash = bonds.hashEnforcementConfig(enforcement);
        return bonds.createBond(
            t,
            enforcement,
            signers,
            _signAccountability(bonds, t, COMPANY_KEY),
            _signAccountability(bonds, t, COUNTERPARTY_KEY)
        );
    }

    function _accountabilitySigners() internal view returns (AccountabilityBondV3Safe.SignerConfig memory signers) {
        signers.attesters = new address[](0);
        signers.adjudicators = new address[](0);
    }

    function _accountabilityEnforcement(
        AccountabilityBondV3Safe.Terms memory t,
        AccountabilityBondV3Safe.SignerConfig memory signers
    ) internal view returns (AccountabilityBondV3Safe.EnforcementConfig memory c) {
        c = AccountabilityBondV3Safe.EnforcementConfig({
            canonicalSource: address(canonicalSource),
            canonicalSourceCodehash: address(canonicalSource).codehash,
            canonicalSourceId: keccak256("canonical-source/base-v3"),
            obligationId: keccak256(abi.encode(t.aiCompany, keccak256("canonical-source/base-v3"))),
            requiredBy: uint64(uint256(t.activePeriodStart) + 1 days),
            responseWindow: 1 days,
            challengeWindow: t.challengeWindow,
            decisionWindow: 1 days,
            maximumSlash: t.companyStake / 2 == 0 ? t.companyStake : t.companyStake / 2,
            evidenceSchemaHash: keccak256("Vaultfire.ObjectiveEvidence.CanonicalRecordedAt.v1"),
            privacyExclusionsHash: keccak256(
                "NO_BEHAVIORAL_SCORING|NO_REPUTATION|NO_LOYALTY|NO_FLOURISHING|NO_QUALITY|NO_BELIEF|NO_BIOMETRIC|NO_PROFILING|NO_SURVEILLANCE"
            ),
            attesterSetHash: bytes32(0),
            adjudicatorSetHash: bytes32(0),
            attesterThreshold: 0,
            adjudicatorThreshold: 0
        });
    }

    function _sortedPair(address a, address b) internal pure returns (address[] memory pair) {
        pair = new address[](2);
        (pair[0], pair[1]) = a < b ? (a, b) : (b, a);
    }

    function _accountabilityInitCode(
        address bondOwner,
        address consensus_,
        address disputes_,
        uint256 perBondCap,
        uint256 aggregateCap,
        address controller_
    ) internal pure returns (bytes memory) {
        disputes_;
        return abi.encodePacked(
            type(AccountabilityBondV3Safe).creationCode,
            abi.encode(bondOwner, consensus_, address(0), perBondCap, aggregateCap, controller_)
        );
    }

    function _fundActivePartnership(PartnershipBondV3Safe bonds, uint256 bondId, uint256 stakeA, uint256 stakeB)
        internal
    {
        vm.deal(partyA, stakeA);
        vm.deal(partyB, stakeB);
        vm.prank(partyA);
        bonds.fundNative{ value: stakeA }(bondId);
        if (stakeB > 0) {
            vm.prank(partyB);
            bonds.fundNative{ value: stakeB }(bondId);
        }
        PartnershipBondV3Safe.BondData memory b = bonds.bondOf(bondId);
        vm.warp(b.terms.activePeriodStart);
        bonds.activate(bondId);
    }

    function _fundActiveAccountability(AccountabilityBondV3Safe bonds, uint256 bondId, uint256 stake) internal {
        vm.deal(company, stake);
        vm.prank(company);
        bonds.fundNative{ value: stake }(bondId);
        AccountabilityBondV3Safe.BondData memory b = bonds.bondOf(bondId);
        vm.warp(b.terms.activePeriodStart);
        bonds.activate(bondId);
    }

    function _signPartnershipMutualSettlement(
        PartnershipBondV3Safe bonds,
        PartnershipBondV3Safe.MutualSettlement memory authorization,
        uint256 privateKey
    ) internal returns (bytes memory) {
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", bonds.DOMAIN_SEPARATOR(), bonds.hashMutualSettlement(authorization))
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
        return abi.encodePacked(r, s, v);
    }
}
