import { BitcoinNetworks } from 'bitcoin-networks';
import { expect } from 'chai';

import {
  BatchFundingGroup,
  OrderIrcInfoV0,
  OrderMetadataV0,
  OrderPositionInfoV0,
} from '../../lib';
import { ContractInfo, ContractInfoV0 } from '../../lib/messages/ContractInfo';
import {
  DlcOffer,
  DlcOfferContainer,
  DlcOfferV0,
  LOCKTIME_THRESHOLD,
} from '../../lib/messages/DlcOffer';
import { FundingInputV0 } from '../../lib/messages/FundingInput';
import { MessageType } from '../../lib/MessageType';

describe('DlcOffer', () => {
  const bitcoinNetwork = BitcoinNetworks.bitcoin_regtest;

  let instance: DlcOfferV0;
  const type = Buffer.from('a71a', 'hex');
  const contractFlags = Buffer.from('00', 'hex');
  const chainHash = Buffer.from(
    '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f',
    'hex',
  );
  const contractInfo = Buffer.from(
    'fdd82e' + // type contract_info
      'fd0131' + // length
      '000000000bebc200' + // total_collateral
      'fda710' + // type contract_descriptor
      '79' + // length
      '03' + // num_outcomes
      'c5a7affd51901bc7a51829b320d588dc7af0ad1f3d56f20a1d3c60c9ba7c6722' + // outcome_1
      '0000000000000000' + // payout_1
      'adf1c23fbeed6611efa5caa0e9ed4c440c450a18bc010a6c867e05873ac08ead' + // outcome_2
      '00000000092363a3' + // payout_2
      '6922250552ad6bb10ab3ddd6981b530aa9a6fd05725bf85b59e3e51163905288' + // outcome_3
      '000000000bebc200' + // payout_3
      'fda712' + // type oracle_info
      'a8' + // length
      'fdd824' + // type oracle_announcement
      'a4' + // length
      'fab22628f6e2602e1671c286a2f63a9246794008627a1749639217f4214cb4a9' + // announcement_signature_r
      '494c93d1a852221080f44f697adb4355df59eb339f6ba0f9b01ba661a8b108d4' + // announcement_signature_s
      'da078bbb1d34e7729e38e2ae34236e776da121af442626fa31e31ae55a279a0b' + // oracle_public_key
      'fdd822' + // type oracle_event
      '40' + // length
      '0001' + // nb_nonces
      '3cfba011378411b20a5ab773cb95daab93e9bcd1e4cce44986a7dda84e01841b' + // oracle_nonces
      '00000000' + // event_maturity_epoch
      'fdd806' + // type enum_event_descriptor
      '10' + // length
      '0002' + // num_outcomes
      '06' + // outcome_1_len
      '64756d6d7931' + // outcome_1
      '06' + // outcome_2_len
      '64756d6d7932' + // outcome_2
      '05' + // event_id_length
      '64756d6d79', // event_id
    'hex',
  );

  const fundingPubKey = Buffer.from(
    '0327efea09ff4dfb13230e887cbab8821d5cc249c7ff28668c6633ff9f4b4c08e3',
    'hex',
  );

  const payoutSPKLen = Buffer.from('0016', 'hex');
  const payoutSPK = Buffer.from(
    '00142bbdec425007dc360523b0294d2c64d2213af498',
    'hex',
  );

  const payoutSerialID = Buffer.from('0000000000b051dc', 'hex');
  const offerCollateralSatoshis = Buffer.from('0000000005f5e0FF', 'hex'); // 99999999
  const fundingInputsLen = Buffer.from('0001', 'hex');

  const fundingInputV0 = Buffer.from(
    'fda714' +
      '3f' + // length
      '000000000000fa51' + // input_serial_id
      '0029' + // prevtx_len
      '02000000000100c2eb0b00000000160014369d63a82ed846f4d47ad55045e594ab95539d6000000000' + // prevtx
      '00000000' + // prevtx_vout
      'ffffffff' + // sequence
      '006b' + // max_witness_len
      '0000', // redeemscript_len
    'hex',
  );

  const changeSPKLen = Buffer.from('0016', 'hex');
  const changeSPK = Buffer.from(
    '0014afa16f949f3055f38bd3a73312bed00b61558884',
    'hex',
  );

  const changeSerialID = Buffer.from('00000000001ea3ed', 'hex');
  const fundOutputSerialID = Buffer.from('000000000052947a', 'hex');
  const feeRatePerVb = Buffer.from('0000000000000001', 'hex');
  const cetLocktime = Buffer.from('00000064', 'hex');
  const refundLocktime = Buffer.from('000000c8', 'hex');

  const dlcOfferHex = Buffer.concat([
    type,
    contractFlags,
    chainHash,
    contractInfo,
    fundingPubKey,
    payoutSPKLen,
    payoutSPK,
    payoutSerialID,
    offerCollateralSatoshis,
    fundingInputsLen,
    fundingInputV0,
    changeSPKLen,
    changeSPK,
    changeSerialID,
    fundOutputSerialID,
    feeRatePerVb,
    cetLocktime,
    refundLocktime,
  ]);

  beforeEach(() => {
    instance = new DlcOfferV0();
    instance.contractFlags = contractFlags;
    instance.chainHash = chainHash;
    instance.contractInfo = ContractInfo.deserialize(contractInfo);
    instance.fundingPubKey = fundingPubKey;
    instance.payoutSPK = payoutSPK;
    instance.payoutSerialId = BigInt(11555292);
    instance.offerCollateralSatoshis = BigInt(99999999);
    instance.fundingInputs = [FundingInputV0.deserialize(fundingInputV0)];
    instance.changeSPK = changeSPK;
    instance.changeSerialId = BigInt(2008045);
    instance.fundOutputSerialId = BigInt(5411962);
    instance.feeRatePerVb = BigInt(1);
    instance.cetLocktime = 100;
    instance.refundLocktime = 200;
  });

  describe('deserialize', () => {
    it('should throw if incorrect type', () => {
      instance.type = 0x123 as MessageType;
      expect(function () {
        DlcOffer.deserialize(instance.serialize());
      }).to.throw(Error);
    });

    it('has correct type', () => {
      expect(DlcOffer.deserialize(instance.serialize()).type).to.equal(
        instance.type,
      );
    });
  });

  describe('DlcOfferV0', () => {
    describe('serialize', () => {
      it('serializes', () => {
        expect(instance.serialize().toString('hex')).to.equal(
          dlcOfferHex.toString('hex'),
        );
      });

      it('serializes with positioninfo', () => {
        const positionInfo = new OrderPositionInfoV0();
        positionInfo.shiftForFees = 'acceptor';
        positionInfo.fees = BigInt(1000);

        instance.positionInfo = positionInfo;
        expect(instance.serialize().toString('hex')).to.equal(
          Buffer.concat([dlcOfferHex, positionInfo.serialize()]).toString(
            'hex',
          ),
        );
      });
    });

    describe('deserialize', () => {
      it('deserializes', () => {
        const instance = DlcOfferV0.deserialize(dlcOfferHex);

        expect(instance.contractFlags).to.deep.equal(contractFlags);
        expect(instance.chainHash).to.deep.equal(chainHash);
        expect(instance.contractInfo.serialize().toString('hex')).to.equal(
          contractInfo.toString('hex'),
        );
        expect(instance.fundingPubKey).to.deep.equal(fundingPubKey);
        expect(instance.payoutSPK).to.deep.equal(payoutSPK);
        expect(Number(instance.payoutSerialId)).to.equal(11555292);
        expect(Number(instance.offerCollateralSatoshis)).to.equal(99999999);
        expect(instance.fundingInputs[0].serialize().toString('hex')).to.equal(
          fundingInputV0.toString('hex'),
        );
        expect(instance.changeSPK).to.deep.equal(changeSPK);
        expect(Number(instance.changeSerialId)).to.equal(2008045);
        expect(Number(instance.fundOutputSerialId)).to.equal(5411962);
        expect(Number(instance.feeRatePerVb)).to.equal(1);
        expect(instance.cetLocktime).to.equal(100);
        expect(instance.refundLocktime).to.equal(200);
      });

      it('has correct type', () => {
        expect(DlcOfferV0.deserialize(dlcOfferHex).type).to.equal(
          MessageType.DlcOfferV0,
        );
      });

      it('deserializes with positioninfo', () => {
        const positionInfo = new OrderPositionInfoV0();
        positionInfo.shiftForFees = 'acceptor';
        positionInfo.fees = BigInt(1000);

        instance.positionInfo = positionInfo;
        expect(
          DlcOfferV0.deserialize(instance.serialize()).positionInfo.serialize(),
        ).to.deep.equal(positionInfo.serialize());
      });
    });

    describe('toJSON', () => {
      it('converts to JSON', async () => {
        const json = instance.toJSON();
        expect(json.type).to.equal(instance.type);
        expect(json.contractFlags).to.equal(
          instance.contractFlags.toString('hex'),
        );
        expect(json.chainHash).to.equal(instance.chainHash.toString('hex'));
        expect(json.contractInfo.type).to.equal(instance.contractInfo.type);
        expect(json.contractInfo.totalCollateral).to.equal(
          Number(instance.contractInfo.totalCollateral),
        );

        expect(json.fundingPubKey).to.equal(
          instance.fundingPubKey.toString('hex'),
        );
        expect(json.payoutSPK).to.equal(instance.payoutSPK.toString('hex'));
        expect(json.payoutSerialId).to.equal(Number(instance.payoutSerialId));
        expect(json.offerCollateralSatoshis).to.equal(
          Number(instance.offerCollateralSatoshis),
        );
        expect(json.fundingInputs[0].type).to.equal(
          instance.fundingInputs[0].type,
        );
        expect(json.fundingInputs[0].inputSerialId).to.equal(
          Number(instance.fundingInputs[0].toJSON().inputSerialId),
        );

        expect(json.changeSPK).to.equal(instance.changeSPK.toString('hex'));
        expect(json.changeSerialId).to.equal(Number(instance.changeSerialId));
        expect(json.fundOutputSerialId).to.equal(
          Number(instance.fundOutputSerialId),
        );
        expect(json.feeRatePerVb).to.equal(Number(instance.feeRatePerVb));
        expect(json.cetLocktime).to.equal(instance.cetLocktime);
        expect(json.refundLocktime).to.equal(instance.refundLocktime);
      });
    });

    describe('getAddresses', () => {
      it('should get addresses', () => {
        const expectedFundingAddress =
          'bcrt1qayylp95g2tzq2a60x2l7f8mclnx5y2jxm0yt09';
        const expectedChangeAddress =
          'bcrt1q47skl9ylxp2l8z7n5ue390kspds4tzyy5jdxs8';
        const expectedPayoutAddress =
          'bcrt1q9w77csjsqlwrvpfrkq556try6gsn4ayc2kn0kl';

        const instance = DlcOfferV0.deserialize(dlcOfferHex);

        const {
          fundingAddress,
          changeAddress,
          payoutAddress,
        } = instance.getAddresses(bitcoinNetwork);

        expect(fundingAddress).to.equal(expectedFundingAddress);
        expect(changeAddress).to.equal(expectedChangeAddress);
        expect(payoutAddress).to.equal(expectedPayoutAddress);
      });
    });

    describe('validate', () => {
      it('should throw if payout_spk is invalid', () => {
        instance.payoutSPK = Buffer.from('fff', 'hex');
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });
      it('should throw if change_spk is invalid', () => {
        instance.changeSPK = Buffer.from('fff', 'hex');
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });
      it('should throw if fundingpubkey is not a valid pubkey', () => {
        instance.fundingPubKey = Buffer.from(
          '00f003aa11f2a97b6be755a86b9fd798a7451c670196a5245b7bae971306b7c87e',
          'hex',
        );
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });
      it('should throw if fundingpubkey is not in compressed secp256k1 format', () => {
        instance.fundingPubKey = Buffer.from(
          '045162991c7299223973cabc99ef5087d7bab2dafe61f78e5388b2f9492f7978123f51fd05ef0693790c0b2d4f30848363a3f3fbcf2bd53a05ba0fd5bb708c3184',
          'hex',
        );
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });
      it('should throw if offerCollateralSatoshis is less than 1000', () => {
        instance.offerCollateralSatoshis = BigInt(999);
        expect(function () {
          instance.validate();
        }).to.throw(Error);

        // boundary check
        instance.offerCollateralSatoshis = BigInt(1000);
        expect(function () {
          instance.validate();
        }).to.not.throw(Error);
      });

      it('should throw if cet_locktime is less than 0', () => {
        instance.cetLocktime = -1;
        expect(() => {
          instance.validate();
        }).to.throw('cet_locktime must be greater than or equal to 0');
      });

      it('should throw if refund_locktime is less than 0', () => {
        instance.refundLocktime = -1;
        expect(() => {
          instance.validate();
        }).to.throw('refund_locktime must be greater than or equal to 0');
      });

      it('should throw if cet_locktime and refund_locktime are not in same units', () => {
        instance.cetLocktime = 100;
        instance.refundLocktime = LOCKTIME_THRESHOLD + 200;
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });

      it('should not throw if cet_locktime and refund_locktime are in same units', () => {
        instance.cetLocktime = 100;
        instance.refundLocktime = 200;
        expect(function () {
          instance.validate();
        }).to.not.throw(Error);

        instance.cetLocktime = LOCKTIME_THRESHOLD + 100;
        instance.refundLocktime = LOCKTIME_THRESHOLD + 200;
        expect(function () {
          instance.validate();
        }).to.not.throw(Error);
      });

      it('should throw if cet_locktime >= refund_locktime', () => {
        instance.cetLocktime = 200;
        instance.refundLocktime = 100;
        expect(function () {
          instance.validate();
        }).to.throw(Error);

        instance.cetLocktime = 100;
        instance.refundLocktime = 100;
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });

      it('should throw if inputSerialIds arent unique', () => {
        instance.fundingInputs = [
          FundingInputV0.deserialize(fundingInputV0),
          FundingInputV0.deserialize(fundingInputV0),
        ];
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });

      it('should throw if changeSerialId  == fundOutputSerialId', () => {
        instance.changeSerialId = instance.fundOutputSerialId;
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });

      it('should throw if totalCollateral <= offerCollateral', () => {
        instance.contractInfo.totalCollateral = BigInt(200000000);
        instance.offerCollateralSatoshis = BigInt(200000000);
        expect(function () {
          instance.validate();
        }).to.throw(Error);

        instance.contractInfo.totalCollateral = BigInt(200000000);
        instance.offerCollateralSatoshis = BigInt(200000001);
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });

      it('should throw if funding amount less than offer collateral satoshis', () => {
        instance.offerCollateralSatoshis = BigInt(3e8);
        expect(function () {
          instance.validate();
        }).to.throw(Error);
      });
    });
  });

  describe('DlcOfferContainer', () => {
    it('should serialize and deserialize', () => {
      const dlcOffer = DlcOfferV0.deserialize(dlcOfferHex);
      // swap payout and change spk to differentiate between dlcoffers
      const dlcOffer2 = DlcOfferV0.deserialize(dlcOfferHex);
      dlcOffer2.payoutSPK = dlcOffer.changeSPK;
      dlcOffer2.changeSPK = dlcOffer.payoutSPK;

      const container = new DlcOfferContainer();
      container.addOffer(dlcOffer);
      container.addOffer(dlcOffer2);

      const instance = DlcOfferContainer.deserialize(container.serialize());

      expect(container.serialize()).to.deep.equal(instance.serialize());
    });
  });

  describe('TLVs', () => {
    it('should deserialize with all TLV types present', () => {
      const contractFlags = Buffer.from('00', 'hex');
      const chainHash = Buffer.from(
        '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f',
        'hex',
      );
      const contractInfo = ContractInfoV0.deserialize(
        Buffer.from(
          'fdd82efd03e80000000000fe502bfda7209c0015fda72684000300fe00f3e2a30000fda72816000200fe00f3e2a30000fe000ea79ffe00f3e2a30000fe000ea79ffe00f3e2a30000fda7281a0002fe000ea79ffe00f3e2a30000fe000f47c8fe00fe502b0000fe000f47c8fe00fe502b0000fda7281a0002fe000f47c8fe00fe502b0000fe001ffffffe00fe502b0000fe001ffffffe00fe502b0000fda7240e000200fd8235fe000b71b0fd411afda712fd033afdd824fd03340c9050848d0ae5294a88020f45ee3c920dcf5c050effa1894480f1c7d6c8fb24ffb379ba9b87f6b4279b104761c776126f85400ea9bb6c6c91c8953af92ae686c3d07289c2ade25405c1c421b38c9322cd73fb2c89f42ce0730a35fae1f8875dfdd822fd02ce001536393424064fae281c59dfce1842dbdb1a696f34620611d25035ed53b8f79f07a855f3c3b2d300d4bfdce1e325b6a59a60272276d89f283db41ca90e3aedbb522fa3c1f8bc08de68c4c5b6ce177f0c62c683fca22b61eccb41fb06bb6e8881d3c23b4d88536e68d65de66e8a7d1c2ec0e6316fdc0d25c4cf42ebaf1775b168fdc847c824efd6f6ae43d9ba0e973de205979971020ebec4919c97cae238513e41e2bc759cd11d213ca2a118e948e0c380eb8bdcf4bcb5eebce6e57c2b22c10fd7083e53643d29206ff66ca5e02485c710e57a1a2de36612323cb7d6ddd1521b4128ce0fb18ff87dda1b796cefa1ec25e89e26f964a0bb6b78423cdf2dbf60df41f6187bdb7e01aeb6c53d7ccb608cc5afbd469e8b45017e1c34bf94dbfb58f3b9a363d4c89bb3ca7f060540b6b03ea6e296381c781fe70d9343aefa095dfdd39dc65f6f748a7a23d4b293ad36e2df52fd988d7a6327088afb483c45b4d21a8e13ef3bec9324cd8b8945dc3c7c534c2c742bd3ecd45bd2c50b78707ea43752ced2620e3cf429ab93c07e137ccf1662376e1d3290818a2020033911deeedc0fe8322091d5bb7b959723c15cb20b7fd2d71abded50cfd4b4db89076d87ad2a0171de9bac64d34e18b7c741618a4e68ca2431631c6fd88b5b90a1d007cc7ac22597df943df27b23d53a1e3a6fe01f5f230a8eb6ae8c7f4c1d40e63d2765250b66f468692bf1b0afa7d7e2ccc8cdc7ca6d691f3327aa3b5b27440ecf3cdcc0da0a9be605f3b4518685624688aa3da90e6a74041cf9330dda94f44d8aabf904e2e31fa54c0503289729cf92bbd63aaf56f97df7baa5a3b677cce06edf54602242671f18695728b465e95fa997a8a4852727e5416433970b902f918485554577129f130b177b7f9d958936c8adf69711e0369f07e8ce92cba06f6e0d91c3f32a8634e52b67046313fdd80a0e00020004626974730000000000151561746f6d69632d656e67696e652d74726164652d31',
          'hex',
        ),
      );
      const fundingPubkey = Buffer.from(
        '03636a2812026c6ea83a6eb27f579ae588aa48dd221fafa049e6613e48ff03953a',
        'hex',
      );
      const payoutSPK = Buffer.from(
        '0014ecf78cf9c4e3cf16b5dccdbd013a7b84cd530d0c',
        'hex',
      );
      const payoutSerialId = BigInt(29829);
      const offerCollateralSatoshis = BigInt(16649967);
      const fundingInput = FundingInputV0.deserialize(
        Buffer.from(
          'fda714fd01fd0000000000a34e3b01e70200000000010369a13b156aff1cf5027140f45d41840c5c552b6d1c512cbb3b1610b850e47ae40000000000feffffff55767d33c145aea3efe723d9803916340fd013ee4f5ad8491e88d38bf8cd180e0000000000feffffff6bab2f95d6b0ca6abf8e8adbec1e70a79bc553c433e1d942f49084f1a1b73bb70100000000feffffff0100e1f50500000000160014bc32a8067fe02ea3a0d1b0daf42e17dd4039992d02473044022055662f02effb509e37bc074c6915eddb09658e6d388e545270ad08ce0bf339a702204b54713323a9bf37e575d4f3349080e4fae1a50762ab2d572967d185a0d16e8901210293914f759527e8e47770750242e7198738705b3a4b2de4f672456497aebf00820247304402202f9d4f5dc184868f866f978f7b4433167d4f50d13329a91eef5f6feec1974019022059b38836960fc3d45fc60021fd2215ad54d2a50224f523e110d48ab63dec4e61012102248d7ea318582d01f9f4c5267a18e28b9a4c03f69877075451f37f4c38af529e0247304402202b592244c641b678d13b059b822754b8f532c0d8ba7ece9a1c3b199b7acea27e02202c506668a40278075d4c6ad68bab0fa8db26fcd1b458aa5becc8a3c845cd86d701210230dbeb555b88731390f0431d3b329002c057cec662d80a8b47f5560433f2efd63104000000000000ffffffff006c0000',
          'hex',
        ),
      );
      const changeSPK = Buffer.from(
        '0014a734d87e6d29d79422f7e5ea7a7709f65dac60e9',
        'hex',
      );
      const changeSerialId = BigInt(94880);
      const fundOutputSerialId = BigInt(44394);
      const feeRatePerVb = BigInt(45);
      const cetLocktime = 1712689645;
      const refundLocktime = 1719255498;
      const metadata = OrderMetadataV0.deserialize(
        Buffer.from('fdf5360f06656e67696e650000000000000000', 'hex'),
      );
      const ircInfo = OrderIrcInfoV0.deserialize(
        Buffer.from(
          'fdf53832104130346a7a504b4b726a6751456f506802dff1fe9bd33ce81881120be26c27d443247bfd3398866a7dc071867e94ff69c9',
          'hex',
        ),
      );
      const positionInfo = OrderPositionInfoV0.deserialize(
        Buffer.from(
          'fdf53a32010000000000001b121561746f6d69632d656e67696e652d74726164652d310000000000fe502b0000000000000000000000',
          'hex',
        ),
      );
      const batchFundingGroup = BatchFundingGroup.deserialize(
        BatchFundingGroup.deserialize(
          Buffer.from(
            'fdff967900000000000005f5e100051561746f6d69632d656e67696e652d74726164652d311561746f6d69632d656e67696e652d74726164652d321561746f6d69632d656e67696e652d74726164652d331561746f6d69632d656e67696e652d74726164652d341561746f6d69632d656e67696e652d74726164652d35',
            'hex',
          ),
        ).serialize(),
      );

      const dlcOffer = new DlcOfferV0();

      dlcOffer.contractFlags = contractFlags;
      dlcOffer.chainHash = chainHash;
      dlcOffer.contractInfo = contractInfo;
      dlcOffer.fundingPubKey = fundingPubkey;
      dlcOffer.payoutSPK = payoutSPK;
      dlcOffer.payoutSerialId = payoutSerialId;
      dlcOffer.offerCollateralSatoshis = offerCollateralSatoshis;
      dlcOffer.fundingInputs = [fundingInput];
      dlcOffer.changeSPK = changeSPK;
      dlcOffer.changeSerialId = changeSerialId;
      dlcOffer.fundOutputSerialId = fundOutputSerialId;
      dlcOffer.feeRatePerVb = feeRatePerVb;
      dlcOffer.cetLocktime = cetLocktime;
      dlcOffer.refundLocktime = refundLocktime;
      dlcOffer.metadata = metadata;
      dlcOffer.ircInfo = ircInfo;
      dlcOffer.positionInfo = positionInfo;
      dlcOffer.batchFundingGroups = [batchFundingGroup];

      expect(dlcOffer.toJSON()).to.deep.equal(
        DlcOfferV0.deserialize(dlcOffer.serialize()).toJSON(),
      );
    });
  });
});
