import BN from 'bn.js';
import { should } from 'chai';
import * as fs from 'fs-extra';
import * as sinon from 'sinon';
import * as tmp from 'tmp';
import { Balance } from '@w3f/polkadot-api-client';
import { Logger, createLogger } from '@w3f/logger';

import { ClientMock } from './mocks';
import { Accountant } from '../src/accountant';
import { Transaction, Claim } from '../src/types';

should();

type checkReceiverInput = {
    senderBalance?: number;
    receiverBalance?: number;
    remaining?: number;
    desired?: number;
    expectedSent?: number;
}

const sandbox = sinon.createSandbox();
const keystore1Content = '{"address": "sender1_addr","encoding":{"content":["", "s325519"]}}'
const tmpobj1 = tmp.fileSync();
fs.writeSync(tmpobj1.fd, keystore1Content);
const keystore1 = {
    filePath: tmpobj1.name,
    passwordPath: "filepassword1"
};
const receiverAddr1 = "receiverAddr1";
const keystore2Content = '{"address": "sender2_addr","encoding":{"content":["", "s325519"]}}'
const tmpobj2 = tmp.fileSync();
fs.writeSync(tmpobj2.fd, keystore2Content);
const keystore2 = {
    filePath: tmpobj2.name,
    passwordPath: "filepassword2"
};
const receiverAddr2 = "receiverAddr2";
const defaultTransactions = (): Transaction[] => [
    {
        sender: {
            keystore: keystore1,
            alias: "sender1",
        },
        receiver: {
            alias: "receiver1",
            address: receiverAddr1
        },
        restriction: {
            remaining: 0,
            desired: 10
        }
    },
    {
        sender: {
            keystore: keystore2,
            alias: "sender2",
        },
        receiver: {
            alias: "receiver2",
            address: receiverAddr2
        },
        restriction: {
            remaining: 0,
            desired: 10
        }
    }
];

const defaultClaims = (): Claim[] => [
    {
        keystore: keystore1,
        alias: "sender1",
    },
    {
        keystore: keystore2,
        alias: "sender2",
    }
];

let logger: Logger;
let client: ClientMock;

const MinimumSenderBalance = 10000000000;

async function checkRestriction(cfg: checkReceiverInput): Promise<void> {
    const txs = defaultTransactions();

    txs.pop();

    txs[0].restriction = {
        remaining: cfg.remaining,
        desired: cfg.desired,
    };

    const subject = new Accountant({transactions:txs, claims:[], minimumSenderBalance:MinimumSenderBalance}, client, logger);

    const sendStub = sandbox.stub(client, 'send');
    const balanceOfKeystoreStub = sandbox.stub(client, 'balanceOfKeystore');
    const balanceOfStub = sandbox.stub(client, 'balanceOf');

    const senderBalance = new BN(cfg.senderBalance) as Balance;
    const receiverBalance = new BN(cfg.receiverBalance) as Balance;
    balanceOfKeystoreStub.onFirstCall().resolves(senderBalance);
    balanceOfStub.onFirstCall().resolves(receiverBalance);

    await subject.run();

    const expectedSent = new BN(cfg.expectedSent) as Balance;
    sendStub.calledWith(keystore1, receiverAddr1, expectedSent).should.be.true;
}

describe('Accountant', () => {
    beforeEach(() => {
        logger = createLogger();
        client = new ClientMock();
    });

    describe('transactions', () => {
        afterEach(() => {
            sandbox.restore();
        });

        it('should process all the transactions in the config', async () => {
            const txs = defaultTransactions();
            const subject = new Accountant({transactions:txs, claims:[], minimumSenderBalance:MinimumSenderBalance}, client, logger);

            const stub = sandbox.stub(client, 'send');

            await subject.run();

            stub.callCount.should.eq(txs.length);
        });

        it('should allow undefined claims', async () => {
            const txs = defaultTransactions();
            const subject = new Accountant({transactions:txs, claims:undefined, minimumSenderBalance:MinimumSenderBalance}, client, logger);

            const stub = sandbox.stub(client, 'send');

            await subject.run();

            stub.callCount.should.eq(txs.length);
        });

        describe('restrictions', () => {
            it('should implement remaining', async () => {
                await checkRestriction({
                    senderBalance: 250000000000000,
                    remaining: 100000000000000,
                    desired: 0,
                    expectedSent: 150000000000000
                });
            });
            it('should implement remaining with undefined desired', async () => {
                await checkRestriction({
                    senderBalance: 250000000000000,
                    remaining: 100000000000000,
                    desired: undefined,
                    expectedSent: 150000000000000
                });
            });
            it('should return 0 if sender balance is less than minimum', async () => {
                const senderBalance = MinimumSenderBalance - 100;
                await checkRestriction({
                    senderBalance,
                    remaining: 0,
                    desired: 0,
                    expectedSent: 0
                });
            });
            it('should return 0 if remaining is less than minimum', async () => {
                const remaining = MinimumSenderBalance - 100;
                await checkRestriction({
                    senderBalance: 200000000000000,
                    remaining,
                    desired: 0,
                    expectedSent: 0
                });
            });
            it('should return 0 if sender balance is less than remaining', async () => {
                await checkRestriction({
                    senderBalance: 9000000000000,
                    remaining: 10000000000000,
                    desired: 0,
                    expectedSent: 0
                });
            });
            it('should implement desired', async () => {
                await checkRestriction({
                    senderBalance: 200000000000000,
                    receiverBalance: 50000000000000,
                    remaining: 0,
                    desired: 100000000000000,
                    expectedSent: 50000000000000
                });
            });
            it('should implement desired with undefined remaining', async () => {
                await checkRestriction({
                    senderBalance: 200000000000000,
                    receiverBalance: 50000000000000,
                    remaining: undefined,
                    desired: 100000000000000,
                    expectedSent: 50000000000000
                });
            });
            it('should return 0 if receiver balance is >= desired', async () => {
                await checkRestriction({
                    senderBalance: 200000000000000,
                    receiverBalance: 300000000000000,
                    remaining: 0,
                    desired: 100000000000000,
                    expectedSent: 0
                });
            });
            it('should send desired on best effort', async () => {
                const senderBalance = 10000000000000;
                const expectedSent = senderBalance - MinimumSenderBalance;
                await checkRestriction({
                    senderBalance,
                    receiverBalance: 50000000000000,
                    remaining: 0,
                    desired: 100000000000000,
                    expectedSent: expectedSent
                });
            });
            it('should return 0 if both remaining and desired are present', async () => {
                await checkRestriction({
                    senderBalance: 1000000000000000,
                    receiverBalance: 50000000000000,
                    remaining: 100000000000000,
                    desired: 100000000000000,
                    expectedSent: 0
                });
            });
        });
        describe('empty actors', () => {
            it('should not try to send when receciver address is empty', async () => {
                const txs = defaultTransactions();

                txs.pop();

                delete txs[0].receiver.address;

                const subject = new Accountant({transactions:txs, claims:[], minimumSenderBalance:MinimumSenderBalance}, client, logger);

                const stub = sandbox.stub(client, 'send');

                await subject.run();

                stub.notCalled.should.be.true;
            });
            it('should get the sender address from the keystore', async () => {
                const senderAddress = 'sender-address-from-keystore';
                const receiverAddress = 'receciver-address-not-from-keystore';

                const fileContent = `{"address":"${senderAddress}","encoded":"encoded-content","encoding":{"content":["pkcs8","ed25519"],"type":"xsalsa20-poly1305","version":"2"},"meta":{}}`;

                const tmpobj = tmp.fileSync();
                fs.writeSync(tmpobj.fd, fileContent);

                const txs = defaultTransactions();

                txs.pop();

                txs[0].sender.keystore = {
                    filePath: tmpobj.name,
                    passwordPath: 'passwordpath'
                };
                txs[0].receiver.address = receiverAddress;

                sandbox.stub(client, 'send');
                const balanceOfStub = sandbox.stub(client, 'balanceOf');
                const balanceOfKeystoreStub = sandbox.stub(client, 'balanceOfKeystore');

                const senderBalance = new BN(100) as Balance;
                const receiverBalance = new BN(100) as Balance;
                balanceOfKeystoreStub.onFirstCall().resolves(senderBalance);
                balanceOfStub.onFirstCall().resolves(receiverBalance);

                const subject = new Accountant({transactions:txs, claims:[], minimumSenderBalance:MinimumSenderBalance}, client, logger);
                await subject.run();

                balanceOfKeystoreStub.calledWith(txs[0].sender.keystore).should.be.true;
            });
        });
    });

    describe('claims', () => {
        afterEach(() => {
            sandbox.restore();
        });

        it('should process all the claims in the config', async () => {
            const claims = defaultClaims();
            const subject = new Accountant({transactions:[], claims, minimumSenderBalance:MinimumSenderBalance}, client, logger);

            const stub = sandbox.stub(client, 'claim');

            await subject.run();

            stub.callCount.should.eq(claims.length);
        });

        it('should allow undefined transactions', async () => {
            const claims = defaultClaims();
            const subject = new Accountant({transactions:undefined, claims, minimumSenderBalance:MinimumSenderBalance}, client, logger);

            const stub = sandbox.stub(client, 'claim');

            await subject.run();

            stub.callCount.should.eq(claims.length);
        });
    });
});