pragma solidity ^0.5.11;

// This contract mocks the Tangerine governance contract ONLY FOR TESTING PURPOSE,
// and its behavior is not exaclty same as the Tangerine governance contract.
contract GovernanceMock {

  struct Node {
    address owner;
    bytes publicKey;
    uint256 staked;
    uint256 fined;
    string name;
    string email;
    string location;
    string url;
    uint256 unstaked;
    uint256 unstakedAt;
  }

  Node[] public nodes;
  mapping(address => int256) public nodesOffsetByAddress;

  mapping(address => bool) public registered;
  uint256 public staked = 0;
  uint256 public unstaked = 0;
  uint256 public minStake = 1000000000000000000;

  event Register(
    address owner,
    bytes publicKey,
    string name,
    string email,
    string location,
    string url,
    uint256 value
  );
  event Staked(address owner, uint256 amount);
  event Unstaked(address owner, uint256 amount);
  event Withdrawn(address owner, uint256 amount);
  event NodeOwnershipTransfered(address indexed NodeAddress, address indexed NewOwnerAddress);

  function register(
    bytes memory publicKey,
    string memory name,
    string memory email,
    string memory location,
    string memory url
  ) public payable {
    require(!registered[msg.sender], "sender address is registered");
    registered[msg.sender] = true;
    staked += msg.value;

    Node memory node = Node(
      msg.sender,
      publicKey,
      msg.value,
      0,
      name,
      email,
      location,
      url,
      0,
      0
    );

    nodesOffsetByAddress[msg.sender] = int(nodes.length);
    nodes.push(node);

    emit Register(msg.sender, publicKey, name, email, location, url, msg.value);
  }

  function stake() public payable {
    staked += msg.value;

    uint256 offset = uint(nodesOffsetByAddress[msg.sender]);
    Node storage node = nodes[offset];
    node.staked += msg.value;

    emit Staked(msg.sender, msg.value);
  }

  function unstake(uint256 amount) public {
    staked -= amount;
    unstaked += amount;

    uint256 offset = uint(nodesOffsetByAddress[msg.sender]);
    Node storage node = nodes[offset];
    require(amount <= node.staked, "unstake amount larger than staked");
    node.staked -= amount;
    node.unstaked += amount;

    emit Unstaked(msg.sender, amount);
  }

  function withdraw() public {
    uint256 offset = uint(nodesOffsetByAddress[msg.sender]);
    Node storage node = nodes[offset];
    uint256 amount = node.unstaked;
    require(amount >= 0, "no unstaked balance");
    require(address(this).balance >= amount, "invalid balance");

    unstaked -= amount;
    node.unstaked = 0;
    msg.sender.transfer(amount);
    emit Withdrawn(msg.sender, amount);
  }

  function withdrawable() public view returns (bool) {
    uint256 offset = uint(nodesOffsetByAddress[msg.sender]);
    Node storage node = nodes[offset];
    uint256 amount = node.unstaked;
    return amount >= 0 && address(this).balance >= amount;
  }

  function replaceNodePublicKey(bytes memory key) public {
    uint256 offset = uint(nodesOffsetByAddress[msg.sender]);
    Node storage node = nodes[offset];
    node.publicKey = key;
  }

  function transferNodeOwnership(address NewOwner) public {
    emit NodeOwnershipTransfered(msg.sender, NewOwner);
  }
}