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

import 'forge-std/Test.sol';
import '../src/Splendid.sol';
import './MintableErc20.sol';
import {ERC1155Holder} from '@openzeppelin/token/ERC1155/utils/ERC1155Holder.sol';
import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {Merkle} from 'murky/Merkle.sol';

contract SplendidTest is Test, ERC1155Holder {
  event CreatorClaimedRewards(uint256 creatorId, address receiver, uint256 amount);
  event CuratorClaimedRewards(address collectionOwner, uint32 collectionIndex, uint256 amount);
  event CollectionCreated(
    address collectionOwner,
    uint32 collectionIndex,
    uint256 curatorFee,
    bytes32 initialRoot,
    uint32 initialContentFeeBips,
    uint32 epochLength,
    uint32 epochZeroStartTime
  );
  event CollectionRootUpdated(
    address collectionOwner,
    uint32 collectionIndex,
    bytes32 root,
    uint32 totalContentFeeBips,
    uint224 epoch,
    bool isActiveRoot
  );
  event UserSubscribed(
    address subscriber,
    address collectionOwner,
    uint32 collectionIndex,
    uint32 subscriptionStartTimestamp,
    uint32 subscriptionDuration,
    uint256 subscriptionFee
  );
  event CreatorRegistered(
    address creator,
    uint256 creatorId
  );

  Splendid public splendid;
  MintableErc20 public mockUsdc;
  uint256 public SUBSCRIPTION_FEE_USDC = 10_000; // $0.01 in USDC.
  uint32 public SUBSCRIPTION_LENGTH_SECONDS = 604_800; // 1 week in seconds.
  uint256 public ORIGINAL_BALANCE = SUBSCRIPTION_FEE_USDC * 10_000;
  uint32 public EPOCH_LENGTH = uint32(60 * 60 * 24 * 7);
  string public MOCK_URI = 'MOCK URI';
  Merkle m = new Merkle();

  constructor() ERC1155Holder() {}

  function setUp() public {
    mockUsdc = new MintableErc20('Mock USDC', 'MOCK');
    splendid = new Splendid(IERC20(mockUsdc), SUBSCRIPTION_FEE_USDC, SUBSCRIPTION_LENGTH_SECONDS, MOCK_URI);
    mockUsdc.mint(address(this), ORIGINAL_BALANCE);
  }

  function getMerkleTreeNodes(Splendid.MerkleTreeNode[] memory nodes) public pure returns (bytes32[] memory) {
    bytes32[] memory nodeHashes = new bytes32[](nodes.length);
    for (uint256 i = 0; i < nodes.length; i++) {
      Splendid.MerkleTreeNode memory node = nodes[i];
      bytes32 nodeHash = keccak256(abi.encodePacked(node.creatorId, node.lastUpdatedEpoch, node.currentFeeBips));
      nodeHashes[i] = nodeHash;
    }
    return nodeHashes;
  }

  function getMerkleTreeData() public view returns (Splendid.MerkleTreeNode[] memory, uint32) {
    uint256 numNodes = 4;
    uint32 totalContentFeeBips = 0;
    Splendid.MerkleTreeNode[] memory nodes = new Splendid.MerkleTreeNode[](numNodes);
    for (uint256 i = 0; i < numNodes; i++) {
      uint256 lastUpdatedEpoch = i == 0 ? 0 : i - 1;
      Splendid.MerkleTreeNode memory node = Splendid.MerkleTreeNode({
        creatorId: i,
        lastUpdatedEpoch: uint224(lastUpdatedEpoch),
        currentFeeBips: splendid.TOTAL_BIPS()
      });
      totalContentFeeBips += node.currentFeeBips;
      nodes[i] = node;
    }
    return (nodes, totalContentFeeBips);
  }

  function testCreateCollection() public {
    createDefaultCollection();
  }

  function testUpdateCollectionSameEpoch() public {
    (Splendid.MerkleTreeNode[] memory nodes, bytes32[] memory nodeHashes, bytes32 root, uint32 totalContentFeeBips, ) = createDefaultCollection();

    // Update the merkle tree data and node hashes.
    nodes[0].currentFeeBips += uint32(splendid.TOTAL_BIPS());
    totalContentFeeBips += uint32(splendid.TOTAL_BIPS());
    nodeHashes = getMerkleTreeNodes(nodes);
    root = m.getRoot(nodeHashes);
    uint32 collectionIndex = 0;

    // Verify a `CollectionRootUpdated` event is emitted.
    vm.expectEmit(false, false, false, true, address(splendid));

    emit CollectionRootUpdated(address(this), collectionIndex, root, totalContentFeeBips, 0, false);

    splendid.updateCollectionProposedRootForEpoch(collectionIndex, root, totalContentFeeBips, 0);
  }

  function testUpdateCollectionNewEpoch() public {
    (Splendid.MerkleTreeNode[] memory nodes, bytes32[] memory nodeHashes, bytes32 root, uint32 totalContentFeeBips, ) = createDefaultCollection();

    // Update the merkle tree data and node hashes after an epoch has passed.
    vm.warp(block.timestamp + uint256(EPOCH_LENGTH));
    nodes[0].currentFeeBips += uint32(splendid.TOTAL_BIPS());
    uint32 newTotalContentFeeBips = totalContentFeeBips + uint32(splendid.TOTAL_BIPS());
    nodeHashes = getMerkleTreeNodes(nodes);
    bytes32 newRoot = m.getRoot(nodeHashes);
    uint32 collectionIndex = 0;

    // Verify two `CollectionRootUpdated` events are emitted.
    vm.expectEmit(false, false, false, true, address(splendid));
    emit CollectionRootUpdated(address(this), collectionIndex, root, totalContentFeeBips, 1, true);

    vm.expectEmit(false, false, false, true, address(splendid));
    emit CollectionRootUpdated(address(this), collectionIndex, newRoot, newTotalContentFeeBips, 1, false);

    splendid.updateCollectionProposedRootForEpoch(collectionIndex, newRoot, newTotalContentFeeBips, 1);
  }

  function testSubscribeToCollectionAndClaimRewards() public {
    (
      Splendid.MerkleTreeNode[] memory nodes,
      bytes32[] memory nodeHashes,
      ,
      uint32 totalContentFeeBips,
      uint256 curatorFee
    ) = createDefaultCollection();

    vm.expectEmit(false, false, false, true, address(splendid));
    emit CreatorRegistered(address(this), nodes[0].creatorId);
    splendid.registerCreator();

    uint32 collectionIndex = 0;

    uint256 expectedFee = (totalContentFeeBips * SUBSCRIPTION_FEE_USDC) / splendid.TOTAL_BIPS() + curatorFee;
    mockUsdc.approve(address(splendid), expectedFee);
    // TODO: Add event emission assertions.
    splendid.subscribeToCollection(address(this), collectionIndex, address(this), address(this));

    assertEq(splendid.getCollectionNumTotalSubscribersAtEpoch(address(this), collectionIndex, 0), 1);

    assertEq(splendid.getCollectionTotalFeeBips(address(this), collectionIndex), totalContentFeeBips);

    assertEq(mockUsdc.balanceOf(address(splendid)), expectedFee);
    assertEq(mockUsdc.balanceOf(address(this)), ORIGINAL_BALANCE - expectedFee);

    // Claim rewards after an epoch has passed.
    vm.warp(block.timestamp + uint256(EPOCH_LENGTH));
    bytes32[] memory proof = m.getProof(nodeHashes, 0);
    splendid.settleCreatorEpochRewardsFromCollection(address(this), collectionIndex, 0, nodes[0], proof, 0, nodes[0], proof);
    assertEq(mockUsdc.balanceOf(address(splendid)), expectedFee);
    assertEq(mockUsdc.balanceOf(address(this)), ORIGINAL_BALANCE - expectedFee);

    // Verify that attempting to settle creator rewards a second time reverts.
    vm.expectRevert(bytes('Already settled creator rewards for this epoch'));
    splendid.settleCreatorEpochRewardsFromCollection(address(this), collectionIndex, 0, nodes[0], proof, 0, nodes[0], proof);

    // Verify a `CreatorClaimedRewards` event is emitted.
    uint256 firstCreatorRewards = (expectedFee - curatorFee) / nodes.length;
    vm.expectEmit(false, false, false, true, address(splendid));
    emit CreatorClaimedRewards(nodes[0].creatorId, address(this), firstCreatorRewards);

    splendid.claimCreatorRewards(nodes[0].creatorId);
    assertEq(mockUsdc.balanceOf(address(splendid)), expectedFee - firstCreatorRewards);
    assertEq(mockUsdc.balanceOf(address(this)), ORIGINAL_BALANCE - expectedFee + firstCreatorRewards);

    // Verify that attempting to claim creator rewards a second time reverts.
    vm.expectRevert(bytes('claimCreatorRewards: no rewards to claim'));
    splendid.claimCreatorRewards(nodes[0].creatorId);

    // Verify a `CuratorClaimedRewards` event is emitted.
    vm.expectEmit(false, false, false, true, address(splendid));
    emit CuratorClaimedRewards(address(this), collectionIndex, curatorFee);

    splendid.claimCuratorRewards(collectionIndex);
    assertEq(mockUsdc.balanceOf(address(splendid)), expectedFee - firstCreatorRewards - curatorFee);
    assertEq(mockUsdc.balanceOf(address(this)), ORIGINAL_BALANCE - expectedFee + firstCreatorRewards + curatorFee);

    // Verify that attempting to claim curator rewards a second time reverts.
    vm.expectRevert(bytes('Provided epochs overlap'));
    splendid.claimCuratorRewards(collectionIndex);
  }

  function createDefaultCollection()
    internal
    returns (
      Splendid.MerkleTreeNode[] memory nodes,
      bytes32[] memory nodeHashes,
      bytes32 root,
      uint32 totalContentFeeBips,
      uint256 curatorFee
    )
  {
    (nodes, totalContentFeeBips) = getMerkleTreeData();
    nodeHashes = getMerkleTreeNodes(nodes);
    root = m.getRoot(nodeHashes);
    curatorFee = 10**6; // $1.

    // Verify a `CollectionCreated` event is emitted.
    vm.expectEmit(false, false, false, true, address(splendid));

    emit CollectionCreated(address(this), 0, curatorFee, root, totalContentFeeBips, EPOCH_LENGTH, uint32(block.timestamp));

    splendid.createCollection(curatorFee, root, totalContentFeeBips, EPOCH_LENGTH);

    assertEq(splendid.getCollectionNumTotalSubscribersAtEpoch(address(this), 0, 0), 0);
    assertEq(splendid.getCollectionTotalFeeBips(address(this), 0), totalContentFeeBips);

    return (nodes, nodeHashes, root, totalContentFeeBips, curatorFee);
  }
}
