// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import 'test/base/TestSetup.t.sol';
import 'contracts/libraries/constants/Types.sol';
import {Typehash} from 'contracts/libraries/constants/Typehash.sol';
import {Typehash as NamespacesTypehash} from 'contracts/namespaces/constants/Typehash.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
import {Address} from '@openzeppelin/contracts/utils/Address.sol';

contract BaseTest is TestSetup {
    using Strings for string;
    using Address for address;

    function testBaseTest() public {
        // Prevents being counted in Foundry Coverage
    }

    // Empty setUp for easier overriding in other tests, otherwise you need to override from TestSetup and is confusing.
    function setUp() public virtual override {
        super.setUp();
    }

    function _effectivelyDisableProfileGuardian(address wallet) internal {
        _effectivelyDisableGuardian(address(hub), wallet);
    }

    function _effectivelyDisableGuardian(address nft, address wallet) internal {
        if (_isProfileGuardianEnabled(wallet)) {
            vm.prank(wallet);
            // TODO: Fix this if we move disableTokenGuardian to its own interface
            LensHub(nft).DANGER__disableTokenGuardian();
            vm.warp(LensHub(nft).getTokenGuardianDisablingTimestamp(wallet));
        }
    }

    function _isProfileGuardianEnabled(address wallet) internal view returns (bool) {
        return
            !wallet.isContract() &&
            (hub.getTokenGuardianDisablingTimestamp(wallet) == 0 ||
                block.timestamp < hub.getTokenGuardianDisablingTimestamp(wallet));
    }

    function _boundPk(uint256 fuzzedUint256) internal view returns (uint256 fuzzedPk) {
        return bound(fuzzedUint256, 1, ISSECP256K1_CURVE_ORDER - 1);
    }

    function _isLensHubProxyAdmin(address proxyAdminCandidate) internal view returns (bool) {
        address proxyAdmin = address(uint160(uint256(vm.load(address(hub), ADMIN_SLOT))));
        return proxyAdminCandidate == proxyAdmin;
    }

    function _getSetProfileMetadataURITypedDataHash(
        uint256 profileId,
        string memory metadataURI,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.SET_PROFILE_METADATA_URI,
                profileId,
                _encodeUsingEip712Rules(metadataURI),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getSetFollowModuleTypedDataHash(
        uint256 profileId,
        address followModule,
        bytes memory followModuleInitData,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.SET_FOLLOW_MODULE,
                profileId,
                followModule,
                _encodeUsingEip712Rules(followModuleInitData),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getChangeDelegatedExecutorsConfigTypedDataHash(
        uint256 delegatorProfileId,
        uint64 configNumber,
        address[] memory delegatedExecutors,
        bool[] memory approvals,
        bool switchToGivenConfig,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.CHANGE_DELEGATED_EXECUTORS_CONFIG,
                delegatorProfileId,
                _encodeUsingEip712Rules(delegatedExecutors),
                _encodeUsingEip712Rules(approvals),
                configNumber,
                switchToGivenConfig,
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getPostTypedDataHash(
        uint256 profileId,
        string memory contentURI,
        address[] memory actionModules,
        bytes[] memory actionModulesInitDatas,
        address referenceModule,
        bytes memory referenceModuleInitData,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.POST,
                profileId,
                _encodeUsingEip712Rules(contentURI),
                _encodeUsingEip712Rules(actionModules),
                _encodeUsingEip712Rules(actionModulesInitDatas),
                referenceModule,
                _encodeUsingEip712Rules(referenceModuleInitData),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getPostTypedDataHash(
        Types.PostParams memory postParams,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        return
            _getPostTypedDataHash({
                profileId: postParams.profileId,
                contentURI: postParams.contentURI,
                actionModules: postParams.actionModules,
                actionModulesInitDatas: postParams.actionModulesInitDatas,
                referenceModule: postParams.referenceModule,
                referenceModuleInitData: postParams.referenceModuleInitData,
                nonce: nonce,
                deadline: deadline
            });
    }

    // We need this to deal with stack too deep:
    struct ReferenceParamsForAbiEncode {
        bytes32 typehash;
        uint256 profileId;
        bytes32 contentURIHash;
        uint256 pointedProfileId;
        uint256 pointedPubId;
        bytes32 referrerProfileIds;
        bytes32 referrerPubIds;
        bytes32 referenceModuleDataHash;
        bytes32 actionModules;
        bytes32 actionModulesInitDataHash;
        address referenceModule;
        bytes32 referenceModuleInitDataHash;
        uint256 nonce;
        uint256 deadline;
    }

    function _abiEncode(
        ReferenceParamsForAbiEncode memory referenceParamsForAbiEncode
    ) private pure returns (bytes memory) {
        return abi.encode(referenceParamsForAbiEncode);
    }

    function _getCommentTypedDataHash(
        Types.CommentParams memory commentParams,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            _abiEncode(
                ReferenceParamsForAbiEncode(
                    Typehash.COMMENT,
                    commentParams.profileId,
                    _encodeUsingEip712Rules(commentParams.contentURI),
                    commentParams.pointedProfileId,
                    commentParams.pointedPubId,
                    _encodeUsingEip712Rules(commentParams.referrerProfileIds),
                    _encodeUsingEip712Rules(commentParams.referrerPubIds),
                    _encodeUsingEip712Rules(commentParams.referenceModuleData),
                    _encodeUsingEip712Rules(commentParams.actionModules),
                    _encodeUsingEip712Rules(commentParams.actionModulesInitDatas),
                    commentParams.referenceModule,
                    _encodeUsingEip712Rules(commentParams.referenceModuleInitData),
                    nonce,
                    deadline
                )
            )
        );
        return _calculateDigest(structHash);
    }

    function _getQuoteTypedDataHash(
        Types.QuoteParams memory quoteParams,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            _abiEncode(
                ReferenceParamsForAbiEncode(
                    Typehash.QUOTE,
                    quoteParams.profileId,
                    _encodeUsingEip712Rules(quoteParams.contentURI),
                    quoteParams.pointedProfileId,
                    quoteParams.pointedPubId,
                    _encodeUsingEip712Rules(quoteParams.referrerProfileIds),
                    _encodeUsingEip712Rules(quoteParams.referrerPubIds),
                    _encodeUsingEip712Rules(quoteParams.referenceModuleData),
                    _encodeUsingEip712Rules(quoteParams.actionModules),
                    _encodeUsingEip712Rules(quoteParams.actionModulesInitDatas),
                    quoteParams.referenceModule,
                    _encodeUsingEip712Rules(quoteParams.referenceModuleInitData),
                    nonce,
                    deadline
                )
            )
        );
        return _calculateDigest(structHash);
    }

    function _getMirrorTypedDataHash(
        Types.MirrorParams memory mirrorParams,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.MIRROR,
                mirrorParams.profileId,
                _encodeUsingEip712Rules(mirrorParams.metadataURI),
                mirrorParams.pointedProfileId,
                mirrorParams.pointedPubId,
                _encodeUsingEip712Rules(mirrorParams.referrerProfileIds),
                _encodeUsingEip712Rules(mirrorParams.referrerPubIds),
                _encodeUsingEip712Rules(mirrorParams.referenceModuleData),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getFollowTypedDataHash(
        uint256 followerProfileId,
        uint256[] memory idsOfProfilesToFollow,
        uint256[] memory followTokenIds,
        bytes[] memory datas,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.FOLLOW,
                followerProfileId,
                _encodeUsingEip712Rules(idsOfProfilesToFollow),
                _encodeUsingEip712Rules(followTokenIds),
                _encodeUsingEip712Rules(datas),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _getActTypedDataHash(
        Types.PublicationActionParams memory publicationActionParams,
        uint256 nonce,
        uint256 deadline
    ) internal view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Typehash.ACT,
                publicationActionParams.publicationActedProfileId,
                publicationActionParams.publicationActedId,
                publicationActionParams.actorProfileId,
                _encodeUsingEip712Rules(publicationActionParams.referrerProfileIds),
                _encodeUsingEip712Rules(publicationActionParams.referrerPubIds),
                publicationActionParams.actionModuleAddress,
                _encodeUsingEip712Rules(publicationActionParams.actionModuleData),
                nonce,
                deadline
            )
        );
        return _calculateDigest(structHash);
    }

    function _calculateDigest(bytes32 structHash) internal view returns (bytes32) {
        return keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash));
    }

    function _getSigStruct(
        uint256 pKey,
        bytes32 digest,
        uint256 deadline
    ) internal pure returns (Types.EIP712Signature memory) {
        return _getSigStruct(vm.addr(pKey), pKey, digest, deadline);
    }

    function _getSigStruct(
        address signer,
        uint256 pKey,
        bytes32 digest,
        uint256 deadline
    ) internal pure returns (Types.EIP712Signature memory) {
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(pKey, digest);
        return Types.EIP712Signature(signer, v, r, s, deadline);
    }

    function _toLegacyV1Pub(uint256 profileId, uint256 pubId, address referenceModule, address collectModule) internal {
        uint256 PUBLICATIONS_MAPPING_SLOT = 20;
        uint256 publicationSlot;

        {
            // NOTE: Quotes are converted into V1 comments.
            Types.PublicationType pubType = hub.getPublicationType(profileId, pubId);
            if (pubType == Types.PublicationType.Nonexistent) {
                revert('Cannot convert unexistent or already V1 publications.');
            } else if (pubType == Types.PublicationType.Mirror && collectModule != address(0)) {
                revert('Legacy V1 mirrors cannot have collect module.');
            } else if (pubType != Types.PublicationType.Mirror && collectModule == address(0)) {
                revert('Legacy V1 non-mirror publications requires a non-zero collect module.');
            }

            assembly {
                mstore(0, profileId)
                mstore(32, PUBLICATIONS_MAPPING_SLOT)
                mstore(32, keccak256(0, 64))
                mstore(0, pubId)
                publicationSlot := keccak256(0, 64)
            }

            uint256 REFERENCE_MODULE_OFFSET = 3;
            uint256 referenceModuleSlot = publicationSlot + REFERENCE_MODULE_OFFSET;
            vm.store({
                target: address(hub),
                slot: bytes32(referenceModuleSlot),
                value: bytes32(uint256(uint160(referenceModule)))
            });

            uint256 COLLECT_MODULE_OFFSET = 4;
            uint256 collectModuleSlot = publicationSlot + COLLECT_MODULE_OFFSET;
            vm.store({
                target: address(hub),
                slot: bytes32(collectModuleSlot),
                value: bytes32(uint256(uint160(collectModule)))
            });

            uint256 firstSlotOffsetToWipe = 5;
            uint256 lastSlotOffsetToWipe = 8;
            for (uint256 offset = firstSlotOffsetToWipe; offset <= lastSlotOffsetToWipe; offset++) {
                vm.store({target: address(hub), slot: bytes32(publicationSlot + offset), value: 0});
            }
        }

        {
            uint256 ACTION_MODULES_OFFSET = 8;
            uint256 ACTION_MODULES_MAPPING_SLOT = publicationSlot + ACTION_MODULES_OFFSET;

            _setActionModuleInPublicationStorage(
                ACTION_MODULES_MAPPING_SLOT,
                _getDefaultPostParams().actionModules[0],
                false
            );
            _setActionModuleInPublicationStorage(
                ACTION_MODULES_MAPPING_SLOT,
                _getDefaultCommentParams().actionModules[0],
                false
            );
            _setActionModuleInPublicationStorage(
                ACTION_MODULES_MAPPING_SLOT,
                _getDefaultQuoteParams().actionModules[0],
                false
            );
        }
    }

    function _setCollectModuleAsIfItWasWhitelistedInLensV1(address collectModule) internal {
        _setCollectModuleWhitelistedInLensV1Storage(collectModule, true);
    }

    function _setCollectModuleWhitelistedInLensV1Storage(address collectModule, bool whitelisted) internal {
        uint256 COLLECT_MODULE_WHITELISTED_MAPPING_SLOT = 15;
        uint256 collectModuleWhitelistSlot;
        assembly {
            mstore(0, collectModule)
            mstore(32, COLLECT_MODULE_WHITELISTED_MAPPING_SLOT)
            collectModuleWhitelistSlot := keccak256(0, 64)
        }
        bytes32 whitelistedAsBytes32 = bytes32(uint256(whitelisted ? 1 : 0));
        vm.store({target: address(hub), slot: bytes32(collectModuleWhitelistSlot), value: whitelistedAsBytes32});
    }

    function _setActionModuleInPublicationStorage(
        uint256 actionModuleMappingSlot,
        address module,
        bool isEnabled
    ) internal {
        bytes32 slot;
        assembly {
            mstore(0, module)
            mstore(32, actionModuleMappingSlot)
            slot := keccak256(0, 64)
        }
        vm.store({target: address(hub), slot: slot, value: bytes32(uint256(isEnabled ? 1 : 0))});
    }

    function _isV1LegacyPub(Types.PublicationMemory memory pub) internal pure returns (bool) {
        return uint8(pub.pubType) == 0;
    }

    function _encodeUsingEip712Rules(bytes[] memory bytesArray) internal pure returns (bytes32) {
        bytes32[] memory bytesArrayEncodedElements = new bytes32[](bytesArray.length);
        uint256 i;
        while (i < bytesArray.length) {
            // A `bytes` type is encoded as its keccak256 hash.
            bytesArrayEncodedElements[i] = keccak256(bytesArray[i]);
            unchecked {
                ++i;
            }
        }
        // An array is encoded as the keccak256 hash of the concatenation of their encoded elements.
        return _encodeUsingEip712Rules(bytesArrayEncodedElements);
    }

    function _encodeUsingEip712Rules(bool[] memory boolArray) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(boolArray));
    }

    function _encodeUsingEip712Rules(address[] memory addressArray) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(addressArray));
    }

    function _encodeUsingEip712Rules(uint256[] memory uint256Array) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(uint256Array));
    }

    function _encodeUsingEip712Rules(bytes32[] memory bytes32Array) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(bytes32Array));
    }

    function _encodeUsingEip712Rules(string memory stringValue) internal pure returns (bytes32) {
        return keccak256(bytes(stringValue));
    }

    function _encodeUsingEip712Rules(bytes memory bytesValue) internal pure returns (bytes32) {
        return keccak256(bytesValue);
    }

    function _transferHandle(address to, uint256 handleId) internal {
        address initialHandleHolder = lensHandles.ownerOf(handleId);
        vm.prank(initialHandleHolder);
        lensHandles.transferFrom(initialHandleHolder, to, handleId);
    }

    function _transferProfile(address to, uint256 profileId) internal {
        address initialProfileHolder = hub.ownerOf(profileId);
        _effectivelyDisableProfileGuardian(initialProfileHolder);
        vm.prank(initialProfileHolder);
        hub.transferFrom(initialProfileHolder, to, profileId);
    }
}
