/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
*/

'use strict';
const sinon = require('sinon');
const chai = require('chai');
const sinonChai = require('sinon-chai');
const expect = chai.expect;

const { Context } = require('fabric-contract-api');
const { ChaincodeStub, ClientIdentity } = require('fabric-shim');

const AssetTransfer = require('../lib/assetTransferEvents.js');

let assert = sinon.assert;
chai.use(sinonChai);

describe('Asset Transfer Events Tests', () => {
	let transactionContext, chaincodeStub, clientIdentity, asset;
	let transientMap, asset_properties;

	beforeEach(() => {
		transactionContext = new Context();

		chaincodeStub = sinon.createStubInstance(ChaincodeStub);
		chaincodeStub.getMspID.returns('org1');
		transactionContext.setChaincodeStub(chaincodeStub);

		clientIdentity = sinon.createStubInstance(ClientIdentity);
		clientIdentity.getMSPID.returns('org1');
		transactionContext.clientIdentity = clientIdentity;

		chaincodeStub.putState.callsFake((key, value) => {
			if (!chaincodeStub.states) {
				chaincodeStub.states = {};
			}
			chaincodeStub.states[key] = value;
		});

		chaincodeStub.getState.callsFake(async (key) => {
			let ret;
			if (chaincodeStub.states) {
				ret = chaincodeStub.states[key];
			}
			return Promise.resolve(ret);
		});

		chaincodeStub.deleteState.callsFake(async (key) => {
			if (chaincodeStub.states) {
				delete chaincodeStub.states[key];
			}
			return Promise.resolve(key);
		});

		chaincodeStub.getStateByRange.callsFake(async () => {
			function* internalGetStateByRange() {
				if (chaincodeStub.states) {
					// Shallow copy
					const copied = Object.assign({}, chaincodeStub.states);

					for (let key in copied) {
						yield {value: copied[key]};
					}
				}
			}

			return Promise.resolve(internalGetStateByRange());
		});

		asset = {
			ID: 'asset1',
			Color: 'blue',
			Size: 5,
			Owner: 'Tomoko',
			AppraisedValue: 300,
		};
		const randomNumber = Math.floor(Math.random() * 100) + 1;
		asset_properties = {
			object_type: 'asset_properties',
			asset_id: 'asset1',
			Price: '90',
			salt: Buffer.from(randomNumber.toString()).toString('hex')
		};
		transientMap = {
			asset_properties: Buffer.from(JSON.stringify(asset_properties))
		};
	});

	describe('Test CreateAsset', () => {
		it('should return error on CreateAsset', async () => {
			chaincodeStub.putState.rejects('failed inserting key');

			let assetTransfer = new AssetTransfer();
			try {
				await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
				assert.fail('CreateAsset should have failed');
			} catch(err) {
				expect(err.name).to.equal('failed inserting key');
			}
		});

		it('should return success on CreateAsset', async () => {
			let assetTransfer = new AssetTransfer();

			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString());
			expect(ret).to.eql(asset);
		});
		it('should return success on CreateAsset with transient data', async () => {
			let assetTransfer = new AssetTransfer();
			chaincodeStub.getTransient.returns(transientMap);
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString());
			expect(ret).to.eql(asset);
		});
	});

	describe('Test ReadAsset', () => {
		it('should return error on ReadAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			try {
				await assetTransfer.ReadAsset(transactionContext, 'asset2');
				assert.fail('ReadAsset should have failed');
			} catch (err) {
				expect(err.message).to.equal('The asset asset2 does not exist');
			}
		});

		it('should return success on ReadAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
			const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1');
			const readAsset = JSON.parse(assetString);
			expect(readAsset).to.eql(asset);
		});

		it('should return success on ReadAsset with private data', async () => {
			asset.asset_properties = asset_properties;
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
			chaincodeStub.getPrivateData.returns(Buffer.from(JSON.stringify(asset_properties)));
			const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1');
			const readAsset = JSON.parse(assetString);
			expect(readAsset).to.eql(asset);
		});
	});

	describe('Test UpdateAsset', () => {
		it('should return error on UpdateAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			try {
				await assetTransfer.UpdateAsset(transactionContext, 'asset2', 'orange', 10, 'Me', 500);
				assert.fail('UpdateAsset should have failed');
			} catch (err) {
				expect(err.message).to.equal('The asset asset2 does not exist');
			}
		});

		it('should return success on UpdateAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			await assetTransfer.UpdateAsset(transactionContext, 'asset1', 'orange', 10, 'Me', 500);
			let ret = JSON.parse(await chaincodeStub.getState(asset.ID));
			let expected = {
				ID: 'asset1',
				Color: 'orange',
				Size: 10,
				Owner: 'Me',
				AppraisedValue: 500
			};
			expect(ret).to.eql(expected);
		});
	});

	describe('Test DeleteAsset', () => {
		it('should return error on DeleteAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			try {
				await assetTransfer.DeleteAsset(transactionContext, 'asset2');
				assert.fail('DeleteAsset should have failed');
			} catch (err) {
				expect(err.message).to.equal('The asset asset2 does not exist');
			}
		});

		it('should return success on DeleteAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			await assetTransfer.DeleteAsset(transactionContext, asset.ID);
			let ret = await chaincodeStub.getState(asset.ID);
			expect(ret).to.equal(undefined);
		});
	});

	describe('Test TransferAsset', () => {
		it('should return error on TransferAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			try {
				await assetTransfer.TransferAsset(transactionContext, 'asset2', 'Me');
				assert.fail('DeleteAsset should have failed');
			} catch (err) {
				expect(err.message).to.equal('The asset asset2 does not exist');
			}
		});

		it('should return success on TransferAsset', async () => {
			let assetTransfer = new AssetTransfer();
			await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);

			await assetTransfer.TransferAsset(transactionContext, asset.ID, 'Me');
			let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString());
			expect(ret).to.eql(Object.assign({}, asset, {Owner: 'Me'}));
		});
	});
});
