// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import "./TestBase.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {FleekAccessControl} from "contracts/FleekAccessControl.sol";
import "contracts/FleekBilling.sol";

contract Test_FleekERC721_BillingAssertions is Test {
    event BillingChanged(FleekBilling.Billing key, uint256 price);
    event Withdrawn(uint256 value, address indexed byAddress);

    function expectRevertWithRequiredPayment(uint256 value) public {
        vm.expectRevert(abi.encodeWithSelector(RequiredPayment.selector, value));
    }

    function expectEmitBillingChanged(FleekBilling.Billing key, uint256 price) public {
        vm.expectEmit(true, true, true, true);
        emit BillingChanged(key, price);
    }

    function expectEmitWithdawn(uint256 value, address byAddress) public {
        vm.expectEmit(true, true, true, true);
        emit Withdrawn(value, byAddress);
    }
}

contract Test_FleekERC721_Billing is Test_FleekERC721_Base, Test_FleekERC721_BillingAssertions {
    using Strings for address;
    uint256 internal tokenId;
    uint256 internal constant mintPrice = 1 ether;
    uint256 internal constant addAPPrice = 1 ether;

    function setUp() public {
        baseSetUp();
        tokenId = mintDefault(deployer);
        CuT.setBilling(FleekBilling.Billing.Mint, mintPrice);
        CuT.setBilling(FleekBilling.Billing.AddAccessPoint, addAPPrice);
    }

    function test_setUp() public {
        assertEq(CuT.getBilling(FleekBilling.Billing.Mint), mintPrice);
        assertEq(CuT.getBilling(FleekBilling.Billing.AddAccessPoint), addAPPrice);
        assertEq(address(CuT).balance, 0);
    }

    function test_mint() public {
        CuT.mint{value: mintPrice}(
            deployer,
            TestConstants.APP_NAME,
            TestConstants.APP_DESCRIPTION,
            TestConstants.APP_EXTERNAL_URL,
            TestConstants.APP_ENS,
            TestConstants.APP_COMMIT_HASH,
            TestConstants.APP_GIT_REPOSITORY,
            TestConstants.LOGO_0,
            TestConstants.APP_COLOR,
            TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,
            deployer
        );
        assertEq(CuT.ownerOf(tokenId), deployer);
        assertEq(address(CuT).balance, mintPrice);
    }

    function testFuzz_cannotMintWithWrongValue(uint256 value) public {
        vm.assume(value != mintPrice);
        vm.deal(deployer, value);
        expectRevertWithRequiredPayment(mintPrice);
        CuT.mint{value: value}(
            deployer,
            TestConstants.APP_NAME,
            TestConstants.APP_DESCRIPTION,
            TestConstants.APP_EXTERNAL_URL,
            TestConstants.APP_ENS,
            TestConstants.APP_COMMIT_HASH,
            TestConstants.APP_GIT_REPOSITORY,
            TestConstants.LOGO_0,
            TestConstants.APP_COLOR,
            TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,
            deployer
        );
        assertEq(address(CuT).balance, 0);
    }

    function testFuzz_shouldChangeMintBillingValue(uint256 value) public {
        expectEmitBillingChanged(FleekBilling.Billing.Mint, value);
        CuT.setBilling(FleekBilling.Billing.Mint, value);

        assertEq(CuT.getBilling(FleekBilling.Billing.Mint), value);

        vm.deal(deployer, value);
        CuT.mint{value: value}(
            deployer,
            TestConstants.APP_NAME,
            TestConstants.APP_DESCRIPTION,
            TestConstants.APP_EXTERNAL_URL,
            TestConstants.APP_ENS,
            TestConstants.APP_COMMIT_HASH,
            TestConstants.APP_GIT_REPOSITORY,
            TestConstants.LOGO_0,
            TestConstants.APP_COLOR,
            TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,
            deployer
        );
        assertEq(CuT.ownerOf(tokenId), deployer);
        assertEq(address(CuT).balance, value);
    }

    function test_addAccessPoint() public {
        CuT.addAccessPoint{value: addAPPrice}(tokenId, "accesspoint.com");
        assertFalse(CuT.isAccessPointNameVerified("accesspoint.com"));
        assertEq(address(CuT).balance, addAPPrice);
    }

    function testFuzz_cannotAddAccessPointWithWrongValue(uint256 value) public {
        vm.assume(value != addAPPrice);
        vm.deal(deployer, value);
        expectRevertWithRequiredPayment(addAPPrice);
        CuT.addAccessPoint{value: value}(tokenId, "accesspoint.com");
        assertEq(address(CuT).balance, 0);
    }

    function testFuzz_shouldChangeAddAPBillingValue(uint256 value) public {
        expectEmitBillingChanged(FleekBilling.Billing.AddAccessPoint, value);
        CuT.setBilling(FleekBilling.Billing.AddAccessPoint, value);

        assertEq(CuT.getBilling(FleekBilling.Billing.AddAccessPoint), value);

        vm.deal(deployer, value);
        CuT.addAccessPoint{value: value}(tokenId, "accesspoint.com");
        assertFalse(CuT.isAccessPointNameVerified("accesspoint.com"));
        assertEq(address(CuT).balance, value);
    }

    function testFuzz_shouldWithdrawAnyContractFunds(uint128 value) public {
        uint256 balanceBefore = address(this).balance;
        vm.deal(address(CuT), value);
        CuT.withdraw();
        assertEq(address(this).balance, value + balanceBefore);
    }

    function testFuzz_shouldWithdrawAllContractFundsAfterPayableCall(uint8 iterations) public {
        // this test is going to add access points up to 256 times and then withdraw all funds
        uint256 balanceBefore = address(this).balance;
        address randomAddress = address(1);
        uint256 totalExpectedValue = iterations * addAPPrice;

        vm.deal(randomAddress, totalExpectedValue);
        vm.startPrank(randomAddress);
        for (uint256 i = 0; i < iterations; i++) {
            CuT.addAccessPoint{value: addAPPrice}(tokenId, Strings.toString(i));
        }
        vm.stopPrank();

        expectEmitWithdawn(totalExpectedValue, deployer);
        CuT.withdraw();
        assertEq(address(this).balance, totalExpectedValue + balanceBefore);
    }

    /**
     * @dev `receive` and `fallback` are required for test contract receive ETH
     */
    receive() external payable {}

    fallback() external payable {}
}
