/* eslint-env mocha */
/* eslint prefer-arrow-callback: "off" */

'use strict';

const assert = require('./util/assert');
const common = require('./util/common');
const Bloom = require('../lib/utils/bloom');
const Block = require('../lib/primitives/block');
const MerkleBlock = require('../lib/primitives/merkleblock');
const consensus = require('../lib/protocol/consensus');
const Script = require('../lib/script/script');
const encoding = require('../lib/utils/encoding');
const bip152 = require('../lib/net/bip152');
const CompactBlock = bip152.CompactBlock;
const TXRequest = bip152.TXRequest;
const TXResponse = bip152.TXResponse;

// Block test vectors
const block300025 = common.readBlock('block300025');

// Merkle block test vectors
const merkle300025 = common.readMerkle('merkle300025');

// Compact block test vectors
const block426884 = common.readBlock('block426884');
const compact426884 = common.readCompact('compact426884');
const block898352 = common.readBlock('block898352');
const compact898352 = common.readCompact('compact898352');

// Sigops counting test vectors
// Format: [name, sigops, weight]
const sigopsVectors = [
  ['block928816', 9109, 3568200],
  ['block928828', 23236, 2481560],
  ['block928831', 10035, 3992382],
  ['block928848', 11319, 3992537],
  ['block928849', 9137, 3682105],
  ['block928927', 10015, 3992391],
  ['block1087400', 1298, 193331]
];

describe('Block', function() {
  this.timeout(10000);

  it('should parse partial merkle tree', () => {
    const [block] = merkle300025.getBlock();

    assert(block.verifyPOW());
    assert(block.verifyBody());
    assert(block.verify());

    const tree = block.getTree();

    assert.strictEqual(tree.matches.length, 2);
    assert.strictEqual(block.hash('hex'),
      '8cc72c02a958de5a8b35a23bb7e3bced8bf840cc0a4e1c820000000000000000');
    assert.strictEqual(block.rhash(),
      '0000000000000000821c4e0acc40f88bedbce3b73ba2358b5ade58a9022cc78c');
    assert.strictEqual(
      tree.matches[0].toString('hex'),
      '7393f84cd04ca8931975c66282ebf1847c78d8de6c2578d4f9bae23bc6f30857');
    assert.strictEqual(
      tree.matches[1].toString('hex'),
      'ec8c51de3170301430ec56f6703533d9ea5b05c6fa7068954bcb90eed8c2ee5c');
  });

  it('should decode/encode merkle block', () => {
    const [block] = merkle300025.getBlock();
    block.refresh();
    assert.bufferEqual(block.toRaw(), merkle300025.getRaw());
  });

  it('should verify merkle block', () => {
    const [block] = merkle300025.getBlock();
    assert(block.verify());
  });

  it('should be encoded/decoded and still verify', () => {
    const [block1] = merkle300025.getBlock();
    const raw = block1.toRaw();
    const block2 = MerkleBlock.fromRaw(raw);
    assert.bufferEqual(block2.toRaw(), raw);
    assert(block2.verify());
  });

  it('should be jsonified/unjsonified and still verify', () => {
    const [block1] = merkle300025.getBlock();
    const json = block1.toJSON();
    const block2 = MerkleBlock.fromJSON(json);
    assert.deepStrictEqual(block2.toJSON(), json);
    assert(block2.verify());
  });

  it('should parse JSON', () => {
    const [block1] = block300025.getBlock();
    const block2 = Block.fromJSON(block1.toJSON());
    assert.strictEqual(block2.hash('hex'),
      '8cc72c02a958de5a8b35a23bb7e3bced8bf840cc0a4e1c820000000000000000');
    assert.strictEqual(block2.rhash(),
      '0000000000000000821c4e0acc40f88bedbce3b73ba2358b5ade58a9022cc78c');
    assert.strictEqual(block2.merkleRoot, block2.createMerkleRoot('hex'));
  });

  it('should create a merkle block', () => {
    const filter = Bloom.fromRate(1000, 0.01, Bloom.flags.NONE);

    const item1 = '8e7445bbb8abd4b3174d80fa4c409fea6b94d96b';
    const item2 = '047b00000078da0dca3b0ec2300c00d0ab4466ed10'
      + 'e763272c6c9ca052972c69e3884a9022084215e2eef'
      + '0e6f781656b5d5a87231cd4349e534b6dea55ad4ff55e';

    filter.add(item1, 'hex');
    filter.add(item2, 'hex');

    const [block1] = block300025.getBlock();
    const block2 = MerkleBlock.fromBlock(block1, filter);

    assert(block2.verifyBody());
    assert.bufferEqual(block2.toRaw(), merkle300025.getRaw());
  });

  it('should verify a historical block', () => {
    const [block, view] = block300025.getBlock();
    const flags = Script.flags.VERIFY_P2SH | Script.flags.VERIFY_DERSIG;
    const height = 300025;

    assert(block.verify());
    assert(block.txs[0].isCoinbase());
    assert(block.txs[0].isSane());
    assert(!block.hasWitness());
    assert.strictEqual(block.getWeight(), 1136924);

    let sigops = 0;
    let reward = 0;

    for (let i = 1; i < block.txs.length; i++) {
      const tx = block.txs[i];

      assert(tx.isSane());
      assert(tx.verifyInputs(view, height));
      assert(tx.verify(view, flags));
      assert(!tx.hasWitness());

      sigops += tx.getSigopsCost(view, flags);
      reward += tx.getFee(view);

      view.addTX(tx, height);
    }

    reward += consensus.getReward(height, 210000);

    assert.strictEqual(sigops, 5280);
    assert.strictEqual(reward, 2507773345);
    assert.strictEqual(reward, block.txs[0].outputs[0].value);
  });

  it('should fail with a bad merkle root', () => {
    const [block] = block300025.getBlock();
    const merkleRoot = block.merkleRoot;
    block.merkleRoot = encoding.NULL_HASH;
    block.refresh();
    assert(!block.verifyPOW());
    const [, reason] = block.checkBody();
    assert.strictEqual(reason, 'bad-txnmrklroot');
    assert(!block.verify());
    block.merkleRoot = merkleRoot;
    block.refresh();
    assert(block.verify());
  });

  it('should fail on merkle block with a bad merkle root', () => {
    const [block] = merkle300025.getBlock();
    const merkleRoot = block.merkleRoot;
    block.merkleRoot = encoding.NULL_HASH;
    block.refresh();
    assert(!block.verifyPOW());
    const [, reason] = block.checkBody();
    assert.strictEqual(reason, 'bad-txnmrklroot');
    assert(!block.verify());
    block.merkleRoot = merkleRoot;
    block.refresh();
    assert(block.verify());
  });

  it('should fail with a low target', () => {
    const [block] = block300025.getBlock();
    const bits = block.bits;
    block.bits = 403014710;
    block.refresh();
    assert(!block.verifyPOW());
    assert(block.verifyBody());
    assert(!block.verify());
    block.bits = bits;
    block.refresh();
    assert(block.verify());
  });

  it('should fail on duplicate txs', () => {
    const [block] = block300025.getBlock();
    block.txs.push(block.txs[block.txs.length - 1]);
    block.refresh();
    const [, reason] = block.checkBody();
    assert.strictEqual(reason, 'bad-txns-duplicate');
  });

  it('should verify with headers', () => {
    const headers = block300025.getHeaders();
    assert(headers.verifyPOW());
    assert(headers.verifyBody());
    assert(headers.verify());
  });

  it('should handle compact block', () => {
    const [block] = block426884.getBlock();
    const [cblock1] = compact426884.getBlock();
    const cblock2 = CompactBlock.fromBlock(block, false, cblock1.keyNonce);

    assert(cblock1.init());

    assert.bufferEqual(cblock1.toRaw(), compact426884.getRaw());
    assert.bufferEqual(cblock2.toRaw(), compact426884.getRaw());

    const map = new Map();

    for (let i = 1; i < block.txs.length; i++) {
      const tx = block.txs[i];
      map.set(tx.hash('hex'), { tx });
    }

    const full = cblock1.fillMempool(false, { map });
    assert(full);

    for (const tx of cblock1.available)
      assert(tx);

    assert.bufferEqual(cblock1.toBlock().toRaw(), block.toRaw());
  });

  it('should handle half-full compact block', () => {
    const [block] = block426884.getBlock();
    const [cblock1] = compact426884.getBlock();
    const cblock2 = CompactBlock.fromBlock(block, false, cblock1.keyNonce);

    assert(cblock1.init());

    assert.bufferEqual(cblock1.toRaw(), compact426884.getRaw());
    assert.bufferEqual(cblock2.toRaw(), compact426884.getRaw());

    const map = new Map();

    for (let i = 1; i < ((block.txs.length + 1) >>> 1); i++) {
      const tx = block.txs[i];
      map.set(tx.hash('hex'), { tx });
    }

    const full = cblock1.fillMempool(false, { map });
    assert(!full);

    const rawReq = cblock1.toRequest().toRaw();
    const req = TXRequest.fromRaw(rawReq);
    assert.strictEqual(req.hash, cblock1.hash('hex'));

    const rawRes = TXResponse.fromBlock(block, req).toRaw();
    const res = TXResponse.fromRaw(rawRes);

    const filled = cblock1.fillMissing(res);
    assert(filled);

    for (const tx of cblock1.available)
      assert(tx);

    assert.bufferEqual(cblock1.toBlock().toRaw(), block.toRaw());
  });

  it('should handle compact block', () => {
    const [block] = block898352.getBlock();
    const [cblock1] = compact898352.getBlock();
    const cblock2 = CompactBlock.fromBlock(block, false, cblock1.keyNonce);

    assert(cblock1.init());

    assert.bufferEqual(cblock1.toRaw(), compact898352.getRaw());
    assert.bufferEqual(cblock2.toRaw(), compact898352.getRaw());

    assert.strictEqual(cblock1.sid(block.txs[1].hash()), 125673511480291);

    const map = new Map();

    for (let i = 1; i < block.txs.length; i++) {
      const tx = block.txs[i];
      map.set(tx.hash('hex'), { tx });
    }

    const full = cblock1.fillMempool(false, { map });
    assert(full);

    for (const tx of cblock1.available)
      assert(tx);

    assert.bufferEqual(cblock1.toBlock().toRaw(), block.toRaw());
  });

  it('should handle half-full compact block', () => {
    const [block] = block898352.getBlock();
    const [cblock1] = compact898352.getBlock();
    const cblock2 = CompactBlock.fromBlock(block, false, cblock1.keyNonce);

    assert(cblock1.init());

    assert.bufferEqual(cblock1.toRaw(), compact898352.getRaw());
    assert.bufferEqual(cblock2.toRaw(), compact898352.getRaw());

    assert.strictEqual(cblock1.sid(block.txs[1].hash()), 125673511480291);

    const map = new Map();

    for (let i = 1; i < ((block.txs.length + 1) >>> 1); i++) {
      const tx = block.txs[i];
      map.set(tx.hash('hex'), { tx });
    }

    const full = cblock1.fillMempool(false, { map });
    assert(!full);

    const rawReq = cblock1.toRequest().toRaw();
    const req = TXRequest.fromRaw(rawReq);
    assert.strictEqual(req.hash, cblock1.hash('hex'));
    assert.deepStrictEqual(req.indexes, [5, 6, 7, 8, 9]);

    const rawRes = TXResponse.fromBlock(block, req).toRaw();
    const res = TXResponse.fromRaw(rawRes);

    const filled = cblock1.fillMissing(res);
    assert(filled);

    for (const tx of cblock1.available)
      assert(tx);

    assert.bufferEqual(cblock1.toBlock().toRaw(), block.toRaw());
  });

  for (const cache of [false, true]) {
    const word = cache ? 'with' : 'without';
    for (const [name, sigops, weight] of sigopsVectors) {
      const ctx = common.readBlock(name);
      it(`should count sigops for ${name} (${word} cache)`, () => {
        const [block, view] = ctx.getBlock();
        const flags = Script.flags.VERIFY_P2SH | Script.flags.VERIFY_WITNESS;

        if (!cache)
          block.refresh(true);

        let count = 0;
        for (const tx of block.txs)
          count += tx.getSigopsCost(view, flags);

        assert.strictEqual(count, sigops);
        assert.strictEqual(block.getWeight(), weight);
      });
    }
  }
});
