package systemtests.test;

import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import duckutil.ConfigMem;
import java.io.File;
import java.security.KeyPair;
import java.util.*;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import snowblossom.client.SnowBlossomClient;
import snowblossom.client.TransactionFactory;
import snowblossom.client.WalletUtil;
import snowblossom.lib.AddressSpecHash;
import snowblossom.lib.AddressUtil;
import snowblossom.lib.ChainHash;
import snowblossom.lib.Globals;
import snowblossom.lib.KeyUtil;
import snowblossom.lib.NetworkParams;
import snowblossom.lib.NetworkParamsRegtest;
import snowblossom.lib.SignatureUtil;
import snowblossom.lib.SnowFall;
import snowblossom.lib.SnowFieldInfo;
import snowblossom.lib.SnowMerkle;
import snowblossom.lib.TransactionBridge;
import snowblossom.lib.TransactionUtil;
import snowblossom.miner.PoolMiner;
import snowblossom.miner.SnowBlossomMiner;
import snowblossom.miner.plow.MrPlow;
import snowblossom.node.AddressHistoryUtil;
import snowblossom.node.ForBenefitOfUtil;
import snowblossom.node.SnowBlossomNode;
import snowblossom.node.TransactionMapUtil;
import snowblossom.proto.*;
import snowblossom.util.proto.*;

public class SpoonTest
{
  @Rule
  public TemporaryFolder test_folder = new TemporaryFolder();

  @BeforeClass
  public static void loadProvider()
  {
    Globals.addCryptoProvider();
  }

  /**
   * More of a giant orbital space platform full of weasels
   * than a unit test
   */
  @Test
  public void spoonTest() throws Exception
  {
    File snow_path = setupSnow();

    Random rnd = new Random();
    int port = 20000 + rnd.nextInt(30000);
    SnowBlossomNode node = startNode(port);
    Thread.sleep(100);

    KeyPair key_pair = KeyUtil.generateECCompressedKey();
    AddressSpec claim = AddressUtil.getSimpleSpecForKey(key_pair.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);
    AddressSpecHash to_addr = AddressUtil.getHashForSpec(claim);

    SnowBlossomMiner miner = startMiner(port, to_addr, snow_path);

    testMinedBlocks(node);

    SnowBlossomClient client = startClient(port);
    testConsolidateFunds(node, client, key_pair, to_addr);

    SnowBlossomNode node2 = startNode(port + 1);
    node2.getPeerage().connectPeer("localhost", port);
    testMinedBlocks(node2);


    miner.stop();
    Thread.sleep(500);
    node.stop();
    node2.stop();
  }

  @Test
  public void fboTest() throws Exception
  {
    File snow_path = setupSnow();

    Random rnd = new Random();
    int port = 20000 + rnd.nextInt(30000);
    SnowBlossomNode node = startNode(port);
    Thread.sleep(100);
    
    SnowBlossomClient client = startClientWithWallet(port);
    SnowBlossomClient client_lock = startClientWithWallet(port);

    WalletDatabase lock_db = genWallet();
    WalletDatabase social_db = genWallet();

    AddressSpecHash mine_to_addr = client.getPurse().getUnusedAddress(false,false);
    AddressSpecHash lock_to_addr = client_lock.getPurse().getUnusedAddress(false, false);

    AddressSpecHash social_addr = AddressUtil.getHashForSpec( social_db.getAddresses(0) );

    
    SnowBlossomMiner miner = startMiner(port, mine_to_addr, snow_path);

    testMinedBlocks(node);

    
    LinkedList<Transaction> tx_list = new LinkedList<>();
    LinkedList<Transaction> tx_list_jumbo = new LinkedList<>();
    LinkedList<Transaction> tx_list_swoopo = new LinkedList<>();

    for(int i=0; i<10; i++)
    {
      TransactionFactoryConfig.Builder config = TransactionFactoryConfig.newBuilder();

      config.setSign(true);
      config.setChangeRandomFromWallet(true);
      config.setInputConfirmedThenPending(true);
      config.setFeeUseEstimate(true);
      config.addOutputs( TransactionOutput.newBuilder()
        .setValue( Globals.SNOW_VALUE)
        .setRecipientSpecHash(lock_to_addr.getBytes())
        .setForBenefitOfSpecHash(social_addr.getBytes())
        .build());
      config.addOutputs( TransactionOutput.newBuilder()
        .setValue( Globals.SNOW_VALUE)
        .setRecipientSpecHash(lock_to_addr.getBytes())
        .setForBenefitOfSpecHash(social_addr.getBytes())
        .setIds( ClaimedIdentifiers.newBuilder().setUsername( ByteString.copyFrom("jumbo".getBytes())) )
        .build());
      config.addOutputs( TransactionOutput.newBuilder()
        .setValue( Globals.SNOW_VALUE)
        .setRecipientSpecHash(lock_to_addr.getBytes())
        .setForBenefitOfSpecHash(social_addr.getBytes())
        .setIds( ClaimedIdentifiers.newBuilder().setChannelname( ByteString.copyFrom("swoopo".getBytes())) )
        .build());
      config.addOutputs( TransactionOutput.newBuilder()
        .setValue( Globals.SNOW_VALUE)
        .setRecipientSpecHash(lock_to_addr.getBytes())
        .setForBenefitOfSpecHash(social_addr.getBytes())
        .setIds( ClaimedIdentifiers.newBuilder().setUsername( ByteString.copyFrom(("name-" + i).getBytes())) )
        .build());

      TransactionFactoryResult tr = TransactionFactory.createTransaction(config.build(), client.getPurse().getDB(), client);

      SubmitReply submit = client.getStub().submitTransaction(tr.getTx());
      System.out.println(submit);

      Assert.assertTrue(submit.getErrorMessage(), submit.getSuccess());

      tx_list.add(tr.getTx());

      waitForMoreBlocks(node, 1);

    }


    // TODO - test FBO
    TxOutList fbo_out_list = ForBenefitOfUtil.getFBOList(social_addr, 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    
    Assert.assertEquals( 40, fbo_out_list.getOutListCount());

    // TODO - test user name
    TxOutList jumbo_list = ForBenefitOfUtil.getIdListUser(ByteString.copyFrom("jumbo".getBytes()), 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    Assert.assertEquals( 10, jumbo_list.getOutListCount());

    // TODO - test channel name
    TxOutList swoopo_list = ForBenefitOfUtil.getIdListChannel(ByteString.copyFrom("swoopo".getBytes()), 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    Assert.assertEquals( 10, swoopo_list.getOutListCount());

    // Make sure the order of these matches the order put onto the chain
    // So the first is the oldest
    for(int i = 0; i<tx_list.size(); i++)
    {
      ByteString tx = tx_list.get(i).getTxHash();
      Assert.assertEquals(tx, jumbo_list.getOutList(i).getTxHash());
      Assert.assertEquals(tx, swoopo_list.getOutList(i).getTxHash());

      Assert.assertEquals(1, ForBenefitOfUtil.getIdListUser(
        ByteString.copyFrom(("name-" + i).getBytes()),
        node.getDB(),
        node.getBlockIngestor().getHead()).getOutListCount());

    }
    
    { // Send back - spend all
      TransactionFactoryConfig.Builder config = TransactionFactoryConfig.newBuilder();

      config.setSign(true);
      config.setChangeRandomFromWallet(true);
      config.setInputConfirmedThenPending(true);
      config.setFeeUseEstimate(true);
      config.setSendAll(true);
      config.addOutputs( TransactionOutput.newBuilder()
        .setValue( 0L )
        .setRecipientSpecHash(mine_to_addr.getBytes())
        .build());

      TransactionFactoryResult tr = TransactionFactory.createTransaction(config.build(), client_lock.getPurse().getDB(), client_lock);

      SubmitReply submit = client_lock.getStub().submitTransaction(tr.getTx());
      System.out.println(submit);

      Assert.assertTrue(submit.getErrorMessage(), submit.getSuccess());

      waitForMoreBlocks(node, 1);



    }
    
    // TODO - test FBO
    TxOutList fbo_out_list_a = ForBenefitOfUtil.getFBOList(social_addr, 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    Assert.assertEquals( 0, fbo_out_list_a.getOutListCount());

    // TODO - test user name
    TxOutList jumbo_list_a = ForBenefitOfUtil.getIdListUser(ByteString.copyFrom("jumbo".getBytes()), 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    Assert.assertEquals( 0, jumbo_list_a.getOutListCount());

    // TODO - test channel name
    TxOutList swoopo_list_a = ForBenefitOfUtil.getIdListChannel(ByteString.copyFrom("swoopo".getBytes()), 
      node.getDB(), 
      node.getBlockIngestor().getHead());
    Assert.assertEquals( 0, swoopo_list_a.getOutListCount());


    miner.stop();
		node.stop();




  }

  @Test
  public void spoonPoolTest() throws Exception
  {
    File snow_path = setupSnow();

    Random rnd = new Random();
    int port = 20000 + rnd.nextInt(30000);
    SnowBlossomNode node = startNode(port);
    Thread.sleep(100);

    KeyPair key_pair = KeyUtil.generateECCompressedKey();
    AddressSpec claim = AddressUtil.getSimpleSpecForKey(key_pair.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);
    AddressSpecHash to_addr = AddressUtil.getHashForSpec(claim);

    KeyPair key_pair2 = KeyUtil.generateECCompressedKey();
    AddressSpec claim2 = AddressUtil.getSimpleSpecForKey(key_pair2.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);
    AddressSpecHash to_addr2 = AddressUtil.getHashForSpec(claim2);

    KeyPair key_pair3 = KeyUtil.generateECCompressedKey();
    AddressSpec claim3 = AddressUtil.getSimpleSpecForKey(key_pair3.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);
    AddressSpecHash to_addr3 = AddressUtil.getHashForSpec(claim3);

    SnowBlossomClient client = startClient(port);

    MrPlow plow = startMrPlow(port, to_addr2);

    PoolMiner miner = startPoolMiner(port+1, to_addr, snow_path);

    waitForMoreBlocks(node, 10);

    System.out.println("ShareMap: " + plow.getShareManager().getShareMap());
    System.out.println("ShareMap pay: " + plow.getShareManager().getPayRatios());

    // Pool getting paid
    waitForFunds(client, to_addr2, 10);

    // Miner getting paid
    waitForFunds(client, to_addr, 30);

    PoolMiner miner2 = startPoolMiner(port+1, to_addr3, snow_path);

    // Second miner getting paid
    waitForFunds(client, to_addr3, 30);
    
    miner.stop();
    miner2.stop();
    Thread.sleep(500);
    plow.stop();
    node.stop();

  }


  @Test
  public void networkReconsileTest() throws Exception
  {
    File snow_path = setupSnow();

    Random rnd = new Random();
    int port = 20000 + rnd.nextInt(30000);
    SnowBlossomNode node1 = startNode(port);
    SnowBlossomNode node2 = startNode(port + 1);
    Thread.sleep(100);

    KeyPair key_pair = KeyUtil.generateECCompressedKey();

    AddressSpec claim = AddressUtil.getSimpleSpecForKey(key_pair.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);

    AddressSpecHash to_addr = AddressUtil.getHashForSpec(claim);

    SnowBlossomMiner miner1 = startMiner(port, to_addr, snow_path);
    SnowBlossomMiner miner2 = startMiner(port + 1, to_addr, snow_path);

    testMinedBlocks(node1);
    testMinedBlocks(node2);

    Assert.assertNotEquals(node1.getDB().getBlockHashAtHeight(2), node2.getDB().getBlockHashAtHeight(2));


    node2.getPeerage().connectPeer("localhost", port);

    Thread.sleep(1000);
    Assert.assertEquals(node1.getDB().getBlockHashAtHeight(2), node2.getDB().getBlockHashAtHeight(2));


    miner1.stop();
    miner2.stop();
    Thread.sleep(500);
    node1.stop();
    node2.stop();

  }

  private void testMinedBlocks(SnowBlossomNode node) throws Exception
  {
    waitForMoreBlocks(node, 3);
  }

  private void waitForMoreBlocks(SnowBlossomNode node, int wait_for) throws Exception
  {
    int start = -1;
    if (node.getBlockIngestor().getHead()!=null)
    {
      start = node.getBlockIngestor().getHead().getHeader().getBlockHeight();
    }
    int target = start + wait_for;

    int height = start;
    for (int i = 0; i < 15; i++)
    {
      Thread.sleep(1000);
      height = node.getBlockIngestor().getHead().getHeader().getBlockHeight();
      if (height >= target) return;
    }
    Assert.fail(String.format("Waiting for %d blocks, only got %d", wait_for, height - start));

  }

  private void waitForFunds(SnowBlossomClient client, AddressSpecHash addr, int max_seconds)
    throws Exception
  {
    for(int i=0; i<max_seconds*10; i++)
    {
      Thread.sleep(100);
      if (client.getSpendable(addr).size() > 0) return;

    }

    Assert.fail(String.format("Waiting for funds.  Didn't get any after %d seconds", max_seconds));

  }


  private void testConsolidateFunds(SnowBlossomNode node, SnowBlossomClient client, KeyPair key_pair, AddressSpecHash from_addr) throws Exception
  {
    List<TransactionBridge> funds = client.getSpendable(from_addr);
    
    System.out.println("Funds: " + funds.size());
    Assert.assertTrue(funds.size() > 3);

    KeyPair key_pair_to = KeyUtil.generateECCompressedKey();

    AddressSpec claim = AddressUtil.getSimpleSpecForKey(key_pair_to.getPublic(), SignatureUtil.SIG_TYPE_ECDSA_COMPRESSED);

    AddressSpecHash to_addr = AddressUtil.getHashForSpec(claim);

    long value = 0;
    LinkedList<TransactionInput> in_list = new LinkedList<>();
    for (TransactionBridge b : funds)
    {
      value += b.value;
      in_list.add(b.in);
    }

    TransactionOutput out = TransactionOutput.newBuilder().setRecipientSpecHash(to_addr.getBytes()).setValue(value).build();

    Transaction tx = TransactionUtil.createTransaction(in_list, ImmutableList.of(out), key_pair);

    Assert.assertTrue(client.submitTransaction(tx));

    waitForMoreBlocks(node, 2);

    List<TransactionBridge> new_funds = client.getSpendable(to_addr);
    Assert.assertEquals(1, new_funds.size());

    TransactionBridge b = new_funds.get(0);
    Assert.assertEquals(value, b.value);

    Assert.assertNotNull(node.getDB());

    TransactionStatus status = TransactionMapUtil.getTxStatus( 
      new ChainHash(tx.getTxHash()), 
      node.getDB(), 
      node.getBlockIngestor().getHead());

    System.out.println(status);
    Assert.assertTrue(status.getConfirmed());

    {
      HistoryList hl = AddressHistoryUtil.getHistory(to_addr, node.getDB(), node.getBlockIngestor().getHead());
      Assert.assertEquals(1, hl.getEntriesCount());
    }
    {
      HistoryList hl = AddressHistoryUtil.getHistory(from_addr, node.getDB(), node.getBlockIngestor().getHead());
      Assert.assertTrue(hl.getEntriesCount()>5);
    }


    System.out.println(tx);

  }

  private File setupSnow() throws Exception
  {
    NetworkParams params = new NetworkParamsRegtest();

    String test_folder_base = test_folder.newFolder().getPath();

    File snow_path = new File(test_folder.newFolder(), "snow");


    for (int i = 0; i < 4; i++)
    {
      SnowFieldInfo info = params.getSnowFieldInfo(i);

      String name = "spoon." + i;

      File field_path = new File(snow_path, name);
      field_path.mkdirs();

      File field = new File(field_path, name + ".snow");

      new SnowFall(field.getPath(), name, info.getLength());
      ByteString root_hash = new SnowMerkle(field_path, name, true).getRootHash();
      Assert.assertEquals(info.getMerkleRootHash(), root_hash);
    }
    return snow_path;

  }

  private SnowBlossomNode startNode(int port) throws Exception
  {

    String test_folder_base = test_folder.newFolder().getPath();

    Map<String, String> config_map = new TreeMap<>();
    config_map.put("db_path", test_folder_base + "/db");
    config_map.put("db_type", "rocksdb");
    config_map.put("service_port", "" + port);
    config_map.put("network", "spoon");
    config_map.put("tx_index", "true");
    config_map.put("addr_index", "true");

    return new SnowBlossomNode(new ConfigMem(config_map));

  }

  private MrPlow startMrPlow(int node_port, AddressSpecHash pool_addr) throws Exception
  {
    String plow_db_path = test_folder.newFolder().getPath();
    Map<String, String> config_map = new TreeMap<>();
    config_map.put("node_host", "localhost");
    config_map.put("node_port", "" + node_port);
    config_map.put("db_type", "rocksdb");
    config_map.put("db_path", plow_db_path +"/plowdb");
    config_map.put("pool_fee", "0.01");
    config_map.put("pool_address", pool_addr.toAddressString(new NetworkParamsRegtest()));
    config_map.put("mining_pool_port", "" +(node_port+1));
    config_map.put("network", "spoon");
    config_map.put("min_diff", "11");

    return new MrPlow(new ConfigMem(config_map));


  }

  private SnowBlossomMiner startMiner(int port, AddressSpecHash mine_to, File snow_path) throws Exception
  {
    Map<String, String> config_map = new TreeMap<>();
    config_map.put("node_host", "localhost");
    config_map.put("node_port", "" + port);
    config_map.put("threads", "1");
    config_map.put("mine_to_address", mine_to.toAddressString(new NetworkParamsRegtest()));
    config_map.put("snow_path", snow_path.getPath());
    config_map.put("network", "spoon");
    if (port % 2 == 1)
    {
      config_map.put("memfield", "true");
    }

    return new SnowBlossomMiner(new ConfigMem(config_map));

  }

  private PoolMiner startPoolMiner(int port, AddressSpecHash mine_to, File snow_path) throws Exception
  {

    
    String addr = mine_to.toAddressString(new NetworkParamsRegtest());
    System.out.println("Starting miner with " + addr);

    Map<String, String> config_map = new TreeMap<>();
    config_map.put("pool_host", "localhost");
    config_map.put("pool_port", "" + port);
    config_map.put("threads", "1");
    config_map.put("mine_to_address", addr);
    config_map.put("snow_path", snow_path.getPath());
    config_map.put("network", "spoon");
    if (port % 2 == 1)
    {
      config_map.put("memfield", "true");
    }

    return new PoolMiner(new ConfigMem(config_map));

  }

  private SnowBlossomClient startClientWithWallet(int port) throws Exception
  {

    String wallet_path = test_folder.newFolder().getPath();

    Map<String, String> config_map = new TreeMap<>();
    config_map.put("node_uri", "grpc://localhost:" + port);
    config_map.put("network", "spoon");
    config_map.put("wallet_path", wallet_path);

    return new SnowBlossomClient(new ConfigMem(config_map));
  }

 

  private SnowBlossomClient startClient(int port) throws Exception
  {

    Map<String, String> config_map = new TreeMap<>();
    config_map.put("node_uri", "grpc://localhost:" + port);
    config_map.put("network", "spoon");

    return new SnowBlossomClient(new ConfigMem(config_map));
  }

  public static WalletDatabase genWallet()
  {
    TreeMap<String,String> config_map = new TreeMap<>();
    config_map.put("key_count", "20");
    WalletDatabase db = WalletUtil.makeNewDatabase(new ConfigMem(config_map), new NetworkParamsRegtest());
    return db;
  }


}