'use strict';

var bn = require('bn.js');
var bcoin = require('../').set('regtest');
var constants = bcoin.constants;
var utils = bcoin.utils;
var crypto = require('../lib/crypto/crypto');
var assert = require('assert');
var opcodes = constants.opcodes;
var co = require('../lib/utils/co');
var cob = co.cob;

describe('Chain', function() {
  var chain, wallet, node, miner, walletdb;
  var tip1, tip2, cb1, cb2;

  this.timeout(5000);

  node = new bcoin.fullnode({ db: 'memory' });
  chain = node.chain;
  walletdb = node.walletdb;
  miner = node.miner;
  node.on('error', function() {});

  var mineBlock = co(function* mineBlock(tip, tx) {
    var attempt = yield miner.createBlock(tip);
    var redeemer;

    if (!tx)
      return attempt.mineSync();

    redeemer = bcoin.mtx();

    redeemer.addOutput({
      address: wallet.receive.getAddress(),
      value: 25 * 1e8
    });

    redeemer.addOutput({
      address: wallet.change.getAddress(),
      value: 5 * 1e8
    });

    redeemer.addInput(tx, 0);

    redeemer.setLocktime(chain.height);

    yield wallet.sign(redeemer);

    attempt.addTX(redeemer.toTX());

    return attempt.mineSync();
  });

  function deleteCoins(tx) {
    var i;

    if (tx.txs) {
      deleteCoins(tx.txs);
      return;
    }

    if (Array.isArray(tx)) {
      for (i = 0; i < tx.length; i++)
        deleteCoins(tx[i]);
      return;
    }

    for (i = 0; i < tx.inputs.length; i++)
      tx.inputs[i].coin = null;
  }

  it('should open chain and miner', cob(function* () {
    miner.mempool = null;
    constants.tx.COINBASE_MATURITY = 0;
    yield node.open();
  }));

  it('should open walletdb', cob(function* () {
    wallet = yield walletdb.create();
    miner.address = wallet.getAddress();
  }));

  it('should mine a block', cob(function* () {
    var block = yield miner.mineBlock();
    assert(block);
  }));

  it('should mine competing chains', cob(function* () {
    var i, block1, block2;

    for (i = 0; i < 10; i++) {
      block1 = yield mineBlock(tip1, cb1);
      cb1 = block1.txs[0];

      block2 = yield mineBlock(tip2, cb2);
      cb2 = block2.txs[0];

      deleteCoins(block1);
      yield chain.add(block1);

      deleteCoins(block2);
      yield chain.add(block2);

      assert(chain.tip.hash === block1.hash('hex'));

      tip1 = yield chain.db.get(block1.hash('hex'));
      tip2 = yield chain.db.get(block2.hash('hex'));

      assert(tip1);
      assert(tip2);

      assert(!(yield tip2.isMainChain()));
    }
  }));

  it('should have correct balance', cob(function* () {
    var balance;

    yield co.timeout(100);

    balance = yield wallet.getBalance();
    assert.equal(balance.unconfirmed, 0);
    assert.equal(balance.confirmed, 500 * 1e8);
    assert.equal(balance.total, 500 * 1e8);
  }));

  it('should handle a reorg', cob(function* () {
    var entry, block, forked;

    assert.equal(walletdb.height, chain.height);
    assert.equal(chain.height, 10);

    entry = yield chain.db.get(tip2.hash);
    assert(entry);
    assert(chain.height === entry.height);

    block = yield miner.mineBlock(entry);
    assert(block);

    forked = false;
    chain.once('reorganize', function() {
      forked = true;
    });

    deleteCoins(block);

    yield chain.add(block);

    assert(forked);
    assert(chain.tip.hash === block.hash('hex'));
    assert(chain.tip.chainwork.cmp(tip1.chainwork) > 0);
  }));

  it('should have correct balance', cob(function* () {
    var balance;

    yield co.timeout(100);

    balance = yield wallet.getBalance();
    assert.equal(balance.unconfirmed, 500 * 1e8);
    assert.equal(balance.confirmed, 550 * 1e8);
    assert.equal(balance.total, 1050 * 1e8);
  }));

  it('should check main chain', cob(function* () {
    var result = yield tip1.isMainChain();
    assert(!result);
  }));

  it('should mine a block after a reorg', cob(function* () {
    var block, entry, result;

    block = yield mineBlock(null, cb2);
    deleteCoins(block);
    yield chain.add(block);

    entry = yield chain.db.get(block.hash('hex'));
    assert(entry);
    assert(chain.tip.hash === entry.hash);

    result = yield entry.isMainChain();
    assert(result);
  }));

  it('should fail to mine a block with coins on an alternate chain', cob(function* () {
    var block = yield mineBlock(null, cb1);
    var err;

    deleteCoins(block);

    try {
      yield chain.add(block);
    } catch (e) {
      err = e;
    }

    assert(err);
    assert.equal(err.reason, 'bad-txns-inputs-missingorspent');
  }));

  it('should get coin', cob(function* () {
    var block, tx, output, coin;

    block = yield mineBlock();
    yield chain.add(block);
    block = yield mineBlock(null, block.txs[0]);
    yield chain.add(block);

    tx = block.txs[1];
    output = bcoin.coin.fromTX(tx, 1);

    coin = yield chain.db.getCoin(tx.hash('hex'), 1);

    assert.deepEqual(coin.toRaw(), output.toRaw());
  }));

  it('should get balance', cob(function* () {
    var balance, txs;

    yield co.timeout(100);

    balance = yield wallet.getBalance();
    assert.equal(balance.unconfirmed, 500 * 1e8);
    assert.equal(balance.confirmed, 700 * 1e8);
    assert.equal(balance.total, 1200 * 1e8);

    assert(wallet.account.receiveDepth >= 8);
    assert(wallet.account.changeDepth >= 7);

    assert.equal(walletdb.height, chain.height);
    assert.equal(walletdb.tip, chain.tip.hash);

    txs = yield wallet.getHistory();
    assert.equal(txs.length, 44);
  }));

  it('should rescan for transactions', cob(function* () {
    var total = 0;
    var hashes = yield walletdb.getHashes();

    yield chain.db.scan(null, hashes, function(block, txs) {
      total += txs.length;
      return Promise.resolve(null);
    });

    assert.equal(total, 25);
  }));

  it('should cleanup', cob(function* () {
    constants.tx.COINBASE_MATURITY = 100;
    yield node.close();
  }));
});
