pragma solidity 0.8.19;

import "./BaseTest.t.sol";
import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol";
import {VRFV2PlusLoadTestWithMetrics} from "../dev/testhelpers/VRFV2PlusLoadTestWithMetrics.sol";
import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol";
import {MockLinkToken} from "../../mocks/MockLinkToken.sol";
import {MockV3Aggregator} from "../../tests/MockV3Aggregator.sol";
import "@openzeppelin/contracts/utils/Strings.sol"; // for Strings.toString
import {VmSafe} from "forge-std/Vm.sol";

contract VRFV2PlusSubscriptionAPITest is BaseTest {
  event SubscriptionFunded(uint256 indexed subId, uint256 oldBalance, uint256 newBalance);
  event SubscriptionFundedWithNative(uint256 indexed subId, uint256 oldNativeBalance, uint256 newNativeBalance);
  event SubscriptionCanceled(uint256 indexed subId, address to, uint256 amountLink, uint256 amountNative);
  event FundsRecovered(address to, uint256 amountLink);
  event NativeFundsRecovered(address to, uint256 amountNative);
  event SubscriptionOwnerTransferRequested(uint256 indexed subId, address from, address to);
  event SubscriptionOwnerTransferred(uint256 indexed subId, address from, address to);
  event SubscriptionConsumerAdded(uint256 indexed subId, address consumer);
  event SubscriptionConsumerRemoved(uint256 indexed subId, address consumer);

  ExposedVRFCoordinatorV2_5 s_subscriptionAPI;

  function setUp() public override {
    BaseTest.setUp();
    address bhs = makeAddr("bhs");
    s_subscriptionAPI = new ExposedVRFCoordinatorV2_5(bhs);
  }

  function testDefaultState() public {
    assertEq(address(s_subscriptionAPI.LINK()), address(0));
    assertEq(address(s_subscriptionAPI.LINK_NATIVE_FEED()), address(0));
    assertEq(s_subscriptionAPI.s_currentSubNonce(), 0);
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0);
    assertEq(s_subscriptionAPI.s_totalBalance(), 0);
    assertEq(s_subscriptionAPI.s_totalNativeBalance(), 0);
  }

  function testSetLINKAndLINKNativeFeed() public {
    address link = makeAddr("link");
    address linkNativeFeed = makeAddr("linkNativeFeed");
    s_subscriptionAPI.setLINKAndLINKNativeFeed(link, linkNativeFeed);
    assertEq(address(s_subscriptionAPI.LINK()), link);
    assertEq(address(s_subscriptionAPI.LINK_NATIVE_FEED()), linkNativeFeed);

    // try setting it again, should revert
    vm.expectRevert(SubscriptionAPI.LinkAlreadySet.selector);
    s_subscriptionAPI.setLINKAndLINKNativeFeed(link, linkNativeFeed);
  }

  function testOwnerCancelSubscriptionNoFunds() public {
    // CASE: new subscription w/ no funds at all
    // Should cancel trivially

    // Note that the link token is not set, but this should still
    // not fail in that case.

    // Create the subscription from a separate address
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // change back to owner and cancel the subscription
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionCanceled(subId, subOwner, 0, 0);
    s_subscriptionAPI.ownerCancelSubscription(subId);

    // assert that the subscription no longer exists
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0));
    // no point in checking s_subscriptions because all fields are zeroed out
    // due to no balance and no requests made
  }

  function testOwnerCancelSubscriptionNativeFundsOnly() public {
    // CASE: new subscription with native funds only
    // no link funds.
    // should cancel and return the native funds

    // Create the subscription from a separate address
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with ether
    vm.deal(subOwner, 10 ether);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFundedWithNative(subId, 0, 5 ether);
    s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId);

    // change back to owner and cancel the subscription
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionCanceled(subId, subOwner, 0 /* link balance */, 5 ether /* native balance */);
    s_subscriptionAPI.ownerCancelSubscription(subId);

    // assert that the subscription no longer exists
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0));
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 0);

    // check the native balance of the subOwner, should be 10 ether
    assertEq(address(subOwner).balance, 10 ether);
  }

  function testOwnerCancelSubscriptionLinkFundsOnly() public {
    // CASE: new subscription with link funds only
    // no native funds.
    // should cancel and return the link funds

    // Create link token and set the link token on the subscription api object
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // Create the subscription from a separate address
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with link
    // can do it from the owner acct because anyone can fund a subscription
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFunded(subId, 0, 5 ether);
    bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId));
    assertTrue(success, "failed link transfer and call");

    // change back to owner and cancel the subscription
    vm.expectEmit(true, false, false, true);
    emit SubscriptionCanceled(subId, subOwner, 5 ether /* link balance */, 0 /* native balance */);
    s_subscriptionAPI.ownerCancelSubscription(subId);

    // assert that the subscription no longer exists
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0));
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 0);

    // check the link balance of the sub owner, should be 5 LINK
    assertEq(linkToken.balanceOf(subOwner), 5 ether);
  }

  function testOwnerCancelSubscriptionNativeAndLinkFunds() public {
    // CASE: new subscription with link and native funds
    // should cancel and return both link and native funds

    // Create link token and set the link token on the subscription api object
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // Create the subscription from a separate address
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with link
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFunded(subId, 0, 5 ether);
    bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId));
    assertTrue(success, "failed link transfer and call");

    // fund the subscription with ether
    vm.deal(subOwner, 10 ether);
    changePrank(subOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFundedWithNative(subId, 0, 5 ether);
    s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId);

    // change back to owner and cancel the subscription
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionCanceled(subId, subOwner, 5 ether /* link balance */, 5 ether /* native balance */);
    s_subscriptionAPI.ownerCancelSubscription(subId);

    // assert that the subscription no longer exists
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, address(0));
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 0);
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 0);

    // check the link balance of the sub owner, should be 5 LINK
    assertEq(linkToken.balanceOf(subOwner), 5 ether, "link balance incorrect");
    // check the ether balance of the sub owner, should be 10 ether
    assertEq(address(subOwner).balance, 10 ether, "native balance incorrect");
  }

  function testRecoverFundsLINKNotSet() public {
    // CASE: link token not set
    // should revert with error LinkNotSet

    // call recoverFunds
    vm.expectRevert(SubscriptionAPI.LinkNotSet.selector);
    s_subscriptionAPI.recoverFunds(OWNER);
  }

  function testRecoverFundsBalanceInvariantViolated() public {
    // CASE: link token set
    // and internal balance is greater than external balance

    // Create link token and set the link token on the subscription api object
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // set the total balance to be greater than the external balance
    // so that we trigger the invariant violation
    // note that this field is not modifiable in the actual contracts
    // other than through onTokenTransfer or similar functions
    s_subscriptionAPI.setTotalBalanceTestingOnlyXXX(100 ether);

    // call recoverFunds
    vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.BalanceInvariantViolated.selector, 100 ether, 0));
    s_subscriptionAPI.recoverFunds(OWNER);
  }

  function testRecoverFundsAmountToTransfer() public {
    // CASE: link token set
    // and internal balance is less than external balance
    // (i.e invariant is not violated)
    // should recover funds successfully

    // Create link token and set the link token on the subscription api object
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // transfer 10 LINK to the contract to recover
    bool success = linkToken.transfer(address(s_subscriptionAPI), 10 ether);
    assertTrue(success, "failed link transfer");

    // call recoverFunds
    vm.expectEmit(true, false, false, true);
    emit FundsRecovered(OWNER, 10 ether);
    s_subscriptionAPI.recoverFunds(OWNER);
  }

  function testRecoverFundsNothingToTransfer() public {
    // CASE: link token set
    // and there is nothing to transfer
    // should do nothing at all

    // Create link token and set the link token on the subscription api object
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // create a subscription and fund it with 5 LINK
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with link
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFunded(subId, 0, 5 ether);
    bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId));
    assertTrue(success, "failed link transfer and call");

    // call recoverFunds, nothing should happen because external balance == internal balance
    s_subscriptionAPI.recoverFunds(OWNER);
    assertEq(linkToken.balanceOf(address(s_subscriptionAPI)), s_subscriptionAPI.s_totalBalance());
  }

  function testRecoverNativeFundsBalanceInvariantViolated() public {
    // set the total balance to be greater than the external balance
    // so that we trigger the invariant violation
    // note that this field is not modifiable in the actual contracts
    // other than through onTokenTransfer or similar functions
    s_subscriptionAPI.setTotalNativeBalanceTestingOnlyXXX(100 ether);

    // call recoverFunds
    vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.BalanceInvariantViolated.selector, 100 ether, 0));
    s_subscriptionAPI.recoverNativeFunds(payable(OWNER));
  }

  function testRecoverNativeFundsAmountToTransfer() public {
    // transfer 10 LINK to the contract to recover
    vm.deal(address(s_subscriptionAPI), 10 ether);

    // call recoverFunds
    vm.expectEmit(true, false, false, true);
    emit NativeFundsRecovered(OWNER, 10 ether);
    s_subscriptionAPI.recoverNativeFunds(payable(OWNER));
  }

  function testRecoverNativeFundsNothingToTransfer() public {
    // create a subscription and fund it with 5 ether
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with ether
    vm.deal(subOwner, 5 ether);
    changePrank(subOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFundedWithNative(subId, 0, 5 ether);
    s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId);

    // call recoverNativeFunds, nothing should happen because external balance == internal balance
    changePrank(OWNER);
    s_subscriptionAPI.recoverNativeFunds(payable(OWNER));
    assertEq(address(s_subscriptionAPI).balance, s_subscriptionAPI.s_totalNativeBalance());
  }

  function testWithdrawNoLink() public {
    // CASE: no link token set
    vm.expectRevert(SubscriptionAPI.LinkNotSet.selector);
    s_subscriptionAPI.withdraw(OWNER);
  }

  function testWithdrawInsufficientBalance() public {
    // CASE: link token set, trying to withdraw
    // more than balance
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // call withdraw
    vm.expectRevert(SubscriptionAPI.InsufficientBalance.selector);
    s_subscriptionAPI.withdraw(OWNER);
  }

  function testWithdrawSufficientBalanceLinkSet() public {
    // CASE: link token set, trying to withdraw
    // less than balance
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // transfer 10 LINK to the contract to withdraw
    bool success = linkToken.transfer(address(s_subscriptionAPI), 10 ether);
    assertTrue(success, "failed link transfer");

    // set the withdrawable tokens of the contract to be 1 ether
    s_subscriptionAPI.setWithdrawableTokensTestingOnlyXXX(1 ether);
    assertEq(s_subscriptionAPI.getWithdrawableTokensTestingOnlyXXX(), 1 ether);

    // set the total balance to be the same as the link balance for consistency
    // (this is not necessary for the test, but just to be sane)
    s_subscriptionAPI.setTotalBalanceTestingOnlyXXX(10 ether);

    // call Withdraw from owner address
    uint256 ownerBalance = linkToken.balanceOf(OWNER);
    changePrank(OWNER);
    s_subscriptionAPI.withdraw(OWNER);
    // assert link balance of owner
    assertEq(linkToken.balanceOf(OWNER) - ownerBalance, 1 ether, "owner link balance incorrect");
    // assert state of subscription api
    assertEq(s_subscriptionAPI.getWithdrawableTokensTestingOnlyXXX(), 0, "owner withdrawable tokens incorrect");
    // assert that total balance is changed by the withdrawn amount
    assertEq(s_subscriptionAPI.s_totalBalance(), 9 ether, "total balance incorrect");
  }

  function testWithdrawNativeInsufficientBalance() public {
    // CASE: trying to withdraw more than balance
    // should revert with InsufficientBalance

    // call WithdrawNative
    changePrank(OWNER);
    vm.expectRevert(SubscriptionAPI.InsufficientBalance.selector);
    s_subscriptionAPI.withdrawNative(payable(OWNER));
  }

  function testWithdrawLinkInvalidOwner() public {
    address invalidAddress = makeAddr("invalidAddress");
    changePrank(invalidAddress);
    vm.expectRevert("Only callable by owner");
    s_subscriptionAPI.withdraw(payable(OWNER));
  }

  function testWithdrawNativeInvalidOwner() public {
    address invalidAddress = makeAddr("invalidAddress");
    changePrank(invalidAddress);
    vm.expectRevert("Only callable by owner");
    s_subscriptionAPI.withdrawNative(payable(OWNER));
  }

  function testWithdrawNativeSufficientBalance() public {
    // CASE: trying to withdraw less than balance
    // should withdraw successfully

    // transfer 10 ether to the contract to withdraw
    vm.deal(address(s_subscriptionAPI), 10 ether);

    // set the withdrawable eth of the contract to be 1 ether
    s_subscriptionAPI.setWithdrawableNativeTestingOnlyXXX(1 ether);
    assertEq(s_subscriptionAPI.getWithdrawableNativeTestingOnlyXXX(), 1 ether);

    // set the total balance to be the same as the eth balance for consistency
    // (this is not necessary for the test, but just to be sane)
    s_subscriptionAPI.setTotalNativeBalanceTestingOnlyXXX(10 ether);

    // call WithdrawNative from owner address
    changePrank(OWNER);
    s_subscriptionAPI.withdrawNative(payable(OWNER));
    // assert native balance
    assertEq(address(OWNER).balance, 1 ether, "owner native balance incorrect");
    // assert state of subscription api
    assertEq(s_subscriptionAPI.getWithdrawableNativeTestingOnlyXXX(), 0, "owner withdrawable native incorrect");
    // assert that total balance is changed by the withdrawn amount
    assertEq(s_subscriptionAPI.s_totalNativeBalance(), 9 ether, "total native balance incorrect");
  }

  function testOnTokenTransferCallerNotLink() public {
    vm.expectRevert(SubscriptionAPI.OnlyCallableFromLink.selector);
    s_subscriptionAPI.onTokenTransfer(makeAddr("someaddress"), 1 ether, abi.encode(uint256(1)));
  }

  function testOnTokenTransferInvalidCalldata() public {
    // create and set link token on subscription api
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // call link.transferAndCall with invalid calldata
    vm.expectRevert(SubscriptionAPI.InvalidCalldata.selector);
    linkToken.transferAndCall(address(s_subscriptionAPI), 1 ether, abi.encode(uint256(1), address(1)));
  }

  function testOnTokenTransferInvalidSubscriptionId() public {
    // create and set link token on subscription api
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // generate bogus sub id
    uint256 subId = uint256(keccak256("idontexist"));

    // try to fund bogus sub id
    vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector);
    linkToken.transferAndCall(address(s_subscriptionAPI), 1 ether, abi.encode(subId));
  }

  function testOnTokenTransferSuccess() public {
    // happy path link funding test
    // create and set link token on subscription api
    MockLinkToken linkToken = new MockLinkToken();
    s_subscriptionAPI.setLINKAndLINKNativeFeed(address(linkToken), address(0));
    assertEq(address(s_subscriptionAPI.LINK()), address(linkToken));

    // create a subscription and fund it with 5 LINK
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with link
    changePrank(OWNER);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFunded(subId, 0, 5 ether);
    bool success = linkToken.transferAndCall(address(s_subscriptionAPI), 5 ether, abi.encode(subId));
    assertTrue(success, "failed link transfer and call");

    // assert that the subscription is funded
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).balance, 5 ether);
  }

  function testFundSubscriptionWithNativeInvalidSubscriptionId() public {
    // CASE: invalid subscription id
    // should revert with InvalidSubscription

    uint256 subId = uint256(keccak256("idontexist"));

    // try to fund the subscription with native, should fail
    address funder = makeAddr("funder");
    vm.deal(funder, 5 ether);
    changePrank(funder);
    vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector);
    s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId);
  }

  function testFundSubscriptionWithNative() public {
    // happy path test
    // funding subscription with native

    // create a subscription and fund it with native
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);

    // fund the subscription with native
    vm.deal(subOwner, 5 ether);
    changePrank(subOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionFundedWithNative(subId, 0, 5 ether);
    s_subscriptionAPI.fundSubscriptionWithNative{value: 5 ether}(subId);

    // assert that the subscription is funded
    assertEq(s_subscriptionAPI.getSubscriptionStruct(subId).nativeBalance, 5 ether);
  }

  function testCreateSubscription() public {
    // test that the subscription is created successfully
    // and test the initial state of the subscription
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);
    assertEq(s_subscriptionAPI.getActiveSubscriptionIdsLength(), 1);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, subOwner);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).requestedOwner, address(0));
  }

  function testCreateSubscriptionRecreate() public {
    // create two subscriptions from the same eoa
    // they should never be the same due to nonce incrementation
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint64 nonceBefore = s_subscriptionAPI.s_currentSubNonce();
    uint256 subId1 = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 1);
    uint256 subId2 = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.s_currentSubNonce(), nonceBefore + 2);
    assertTrue(subId1 != subId2);
  }

  function testSubscriptionOwnershipTransfer() public {
    // create two eoa's, and create a subscription from one of them
    // and transfer ownership to the other
    // assert that the subscription is now owned by the other eoa
    address oldOwner = makeAddr("oldOwner");
    address newOwner = makeAddr("newOwner");

    // create sub
    changePrank(oldOwner);
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).owner, oldOwner);

    // request ownership transfer
    changePrank(oldOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionOwnerTransferRequested(subId, oldOwner, newOwner);
    s_subscriptionAPI.requestSubscriptionOwnerTransfer(subId, newOwner);

    // accept ownership transfer from newOwner
    changePrank(newOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionOwnerTransferred(subId, oldOwner, newOwner);
    s_subscriptionAPI.acceptSubscriptionOwnerTransfer(subId);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).requestedOwner, address(0));
  }

  function testAddConsumerTooManyConsumers() public {
    // add 100 consumers to a sub and then
    // try adding one more and see the revert
    address subOwner = makeAddr("subOwner");
    changePrank(subOwner);
    uint256 subId = s_subscriptionAPI.createSubscription();
    for (uint256 i = 0; i < 100; i++) {
      address consumer = makeAddr(Strings.toString(i));
      vm.expectEmit(true, false, false, true);
      emit SubscriptionConsumerAdded(subId, consumer);
      s_subscriptionAPI.addConsumer(subId, consumer);
    }

    // try adding one more consumer, should revert
    address lastConsumer = makeAddr("consumer");
    changePrank(subOwner);
    vm.expectRevert(SubscriptionAPI.TooManyConsumers.selector);
    s_subscriptionAPI.addConsumer(subId, lastConsumer);
  }

  function testAddConsumerReaddSameConsumer() public {
    // try adding the same consumer twice
    // should be a no-op
    // assert state is unchanged after the 2nd add
    address subOwner = makeAddr("subOwner");
    address consumer = makeAddr("consumer");
    changePrank(subOwner);
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0);
    changePrank(subOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionConsumerAdded(subId, consumer);
    s_subscriptionAPI.addConsumer(subId, consumer);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer);

    // add consumer again, should be no-op
    changePrank(subOwner);
    VmSafe.Log[] memory events = vm.getRecordedLogs();
    s_subscriptionAPI.addConsumer(subId, consumer);
    assertEq(events.length, 0);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer);

    // remove consumer
    vm.expectEmit(true, false, false, true);
    emit SubscriptionConsumerRemoved(subId, consumer);
    s_subscriptionAPI.removeConsumer(subId, consumer);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0);

    // removing consumer twice should revert
    vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.InvalidConsumer.selector, subId, address(consumer)));
    s_subscriptionAPI.removeConsumer(subId, consumer);

    //re-add consumer
    vm.expectEmit(true, false, false, true);
    emit SubscriptionConsumerAdded(subId, consumer);
    s_subscriptionAPI.addConsumer(subId, consumer);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer);
  }

  function testAddConsumer() public {
    // create a subscription and add a consumer
    // assert subscription state afterwards
    address subOwner = makeAddr("subOwner");
    address consumer = makeAddr("consumer");
    changePrank(subOwner);
    uint256 subId = s_subscriptionAPI.createSubscription();
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 0);

    // only subscription owner can add a consumer
    address notSubOwner = makeAddr("notSubOwner");
    changePrank(notSubOwner);
    vm.expectRevert(abi.encodeWithSelector(SubscriptionAPI.MustBeSubOwner.selector, subOwner));
    s_subscriptionAPI.addConsumer(subId, consumer);

    // subscription owner is able to add a consumer
    changePrank(subOwner);
    vm.expectEmit(true, false, false, true);
    emit SubscriptionConsumerAdded(subId, consumer);
    s_subscriptionAPI.addConsumer(subId, consumer);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers.length, 1);
    assertEq(s_subscriptionAPI.getSubscriptionConfig(subId).consumers[0], consumer);
  }
}
