import { IgxTransactionService } from './igx-transaction';
import { Transaction, TransactionType, HierarchicalTransaction } from './transaction';
import { SampleTestData } from '../../test-utils/sample-test-data.spec';
import { IgxHierarchicalTransactionService } from './igx-hierarchical-transaction';

describe('IgxTransaction', () => {
    describe('IgxTransaction UNIT tests', () => {
        it('Should initialize transactions log properly', () => {
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();
            expect(trans['_transactions']).toBeDefined();
            expect(trans['_transactions'].length).toEqual(0);
            expect(trans['_redoStack']).toBeDefined();
            expect(trans['_redoStack'].length).toEqual(0);
            expect(trans['_states']).toBeDefined();
            expect(trans['_states'].size).toEqual(0);
        });

        it('Should add transactions to the transactions log', () => {
            const trans = new IgxTransactionService();
            const transactions: Transaction[] = [
                { id: '1', type: TransactionType.ADD, newValue: 1 },
                { id: '2', type: TransactionType.ADD, newValue: 2 },
                { id: '3', type: TransactionType.ADD, newValue: 3 },
                { id: '1', type: TransactionType.UPDATE, newValue: 4 },
                { id: '5', type: TransactionType.ADD, newValue: 5 },
                { id: '6', type: TransactionType.ADD, newValue: 6 },
                { id: '2', type: TransactionType.DELETE, newValue: 7 },
                { id: '8', type: TransactionType.ADD, newValue: 8 },
                { id: '9', type: TransactionType.ADD, newValue: 9 },
                { id: '8', type: TransactionType.UPDATE, newValue: 10 }
            ];
            expect(trans['_transactions'].length).toEqual(0);
            expect(trans['_redoStack'].length).toEqual(0);
            let transactionIndex = 1;
            transactions.forEach((transaction) => {
                trans.add(transaction);
                expect(trans.getTransactionLog(transaction.id).pop()).toEqual(transaction);
                expect(trans['_transactions'].length).toEqual(transactionIndex);
                expect(trans['_redoStack'].length).toEqual(0);
                transactionIndex++;
            });
        });

        it('Should throw an error when trying to add duplicate transaction', () => {
            const trans = new IgxTransactionService();
            const transactions: Transaction[] = [
                { id: '1', type: TransactionType.ADD, newValue: 1 },
                { id: '2', type: TransactionType.ADD, newValue: 2 },
                { id: '3', type: TransactionType.ADD, newValue: 3 },
                { id: '1', type: TransactionType.UPDATE, newValue: 4 },
                { id: '5', type: TransactionType.ADD, newValue: 5 },
                { id: '6', type: TransactionType.ADD, newValue: 6 },
                { id: '2', type: TransactionType.DELETE, newValue: 7 },
                { id: '8', type: TransactionType.ADD, newValue: 8 },
                { id: '9', type: TransactionType.ADD, newValue: 9 },
                { id: '8', type: TransactionType.UPDATE, newValue: 10 }
            ];
            transactions.forEach(t => trans.add(t));

            const transaction = { id: '6', type: TransactionType.ADD, newValue: 6 };
            expect(trans.getTransactionLog('6').pop()).toEqual(transaction);
            const msg = `Cannot add this transaction. Transaction with id: ${transaction.id} has been already added.`;
            expect(() => trans.add(transaction)).toThrowError(msg);
        });

        it('Should throw an error when trying to update transaction with no recordRef', () => {
            const trans = new IgxTransactionService();
            const transactions: Transaction[] = [
                { id: '1', type: TransactionType.ADD, newValue: 1 },
                { id: '2', type: TransactionType.ADD, newValue: 2 },
                { id: '3', type: TransactionType.ADD, newValue: 3 },
                { id: '1', type: TransactionType.UPDATE, newValue: 4 },
                { id: '5', type: TransactionType.ADD, newValue: 5 },
                { id: '6', type: TransactionType.ADD, newValue: 6 },
                { id: '2', type: TransactionType.DELETE, newValue: 7 },
                { id: '8', type: TransactionType.ADD, newValue: 8 },
                { id: '9', type: TransactionType.ADD, newValue: 9 },
                { id: '8', type: TransactionType.UPDATE, newValue: 10 }
            ];
            transactions.forEach(transaction => trans.add(transaction));

            const updateTransaction = { id: '2', type: TransactionType.DELETE, newValue: 7 };
            expect(trans.getTransactionLog('2').pop()).toEqual(updateTransaction);
            const msg = `Cannot add this transaction. This is first transaction of type ${updateTransaction.type} ` +
                `for id ${updateTransaction.id}. For first transaction of this type recordRef is mandatory.`;
            expect(() => {
                updateTransaction.newValue = 107;
                trans.add(updateTransaction);
            }).toThrowError(msg);
        });

        it('Should throw an error when trying to delete an already deleted item', () => {
            const trans = new IgxTransactionService();
            const recordRef = { key: 'Key1', value: 1 };
            const deleteTransaction: Transaction = { id: 'Key1', type: TransactionType.DELETE, newValue: null };
            trans.add(deleteTransaction, recordRef);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);

            const msg = `Cannot add this transaction. Transaction with id: ${deleteTransaction.id} has been already deleted.`;
            expect(() => trans.add(deleteTransaction)).toThrowError(msg);
        });

        it('Should throw an error when trying to update an already deleted item', () => {
            const trans = new IgxTransactionService();
            const recordRef = { key: 'Key1', value: 1 };
            const deleteTransaction: Transaction = { id: 'Key1', type: TransactionType.DELETE, newValue: null };
            trans.add(deleteTransaction, recordRef);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);

            const msg = `Cannot add this transaction. Transaction with id: ${deleteTransaction.id} has been already deleted.`;
            expect(() => {
                deleteTransaction.type = TransactionType.UPDATE;
                deleteTransaction.newValue = 5;
                trans.add(deleteTransaction);
            }).toThrowError(msg);
        });

        it('Should get a transaction by transaction id', () => {
            const trans = new IgxTransactionService();
            let transaction: Transaction = { id: '0', type: TransactionType.ADD, newValue: 0 };
            trans.add(transaction);
            expect(trans.getTransactionLog('0').pop()).toEqual(transaction);
            transaction = { id: '1', type: TransactionType.ADD, newValue: 1 };
            trans.add(transaction);
            expect(trans.getTransactionLog('1').pop()).toEqual(transaction);
            transaction = { id: '2', type: TransactionType.ADD, newValue: 2 };
            trans.add(transaction);
            expect(trans.getTransactionLog('2').pop()).toEqual(transaction);
            transaction = { id: '3', type: TransactionType.ADD, newValue: 3 };
            trans.add(transaction);
            expect(trans.getTransactionLog('3').pop()).toEqual(transaction);
            transaction = { id: '1', type: TransactionType.UPDATE, newValue: 4 };
            trans.add(transaction);
            expect(trans.getTransactionLog('1').pop()).toEqual(transaction);
            transaction = { id: '5', type: TransactionType.ADD, newValue: 5 };
            trans.add(transaction);
            expect(trans.getTransactionLog('5').pop()).toEqual(transaction);
            transaction = { id: '6', type: TransactionType.ADD, newValue: 6 };
            trans.add(transaction);
            expect(trans.getTransactionLog('6').pop()).toEqual(transaction);
            transaction = { id: '2', type: TransactionType.DELETE, newValue: 7 };
            trans.add(transaction);
            expect(trans.getTransactionLog('2').pop()).toEqual(transaction);
            transaction = { id: '8', type: TransactionType.ADD, newValue: 8 };
            trans.add(transaction);
            expect(trans.getTransactionLog('8').pop()).toEqual(transaction);
            transaction = { id: '9', type: TransactionType.ADD, newValue: 9 };
            trans.add(transaction);
            expect(trans.getTransactionLog('9').pop()).toEqual(transaction);
            transaction = { id: '8', type: TransactionType.UPDATE, newValue: 10 };
            trans.add(transaction);
            expect(trans.getTransactionLog('8').pop()).toEqual(transaction);

            // Get nonexisting transaction
            expect(trans.getTransactionLog('100').pop()).toEqual(undefined);
        });

        it('Should add ADD type transaction - all feasible paths, and correctly fires onStateUpdate', () => {
            const trans = new IgxTransactionService();
            spyOn(trans.onStateUpdate, 'emit').and.callThrough();
            expect(trans).toBeDefined();

            // ADD
            const addTransaction: Transaction = { id: 0, type: TransactionType.ADD, newValue: 1 };
            trans.add(addTransaction);
            expect(trans.getAggregatedValue(0, true)).toEqual(1);
            expect(trans.getTransactionLog(0).pop()).toEqual(addTransaction);
            expect(trans.getTransactionLog()).toEqual([addTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: addTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(1);

            trans.clear();
            expect(trans.getState(0)).toBeUndefined();
            expect(trans.getAggregatedValue(0, true)).toBeNull();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(2);

            // ADD -> Undo
            trans.add(addTransaction);
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(4);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(5);

            // ADD -> Undo -> Redo
            trans.add(addTransaction);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog()).toEqual([addTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: addTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(8);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(9);

            // ADD -> DELETE
            trans.add(addTransaction);
            const deleteTransaction: Transaction = { id: 0, type: TransactionType.DELETE, newValue: 1 };
            trans.add(deleteTransaction);
            expect(trans.getTransactionLog()).toEqual([addTransaction, deleteTransaction]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(11);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(12);

            // ADD -> DELETE -> Undo
            trans.add(addTransaction);
            trans.add(deleteTransaction);
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([addTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: addTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(15);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(16);

            // ADD -> DELETE -> Undo -> Redo
            trans.add(addTransaction);
            trans.add(deleteTransaction);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog()).toEqual([addTransaction, deleteTransaction]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(20);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(21);

            // ADD -> DELETE -> Undo -> Undo
            trans.add(addTransaction);
            trans.add(deleteTransaction);
            trans.undo();
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(25);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(26);

            // ADD -> UPDATE
            trans.add(addTransaction);
            const updateTransaction: Transaction = { id: 0, type: TransactionType.UPDATE, newValue: 2 };
            trans.add(updateTransaction);
            expect(trans.getTransactionLog()).toEqual([addTransaction, updateTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: updateTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(28);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(29);

            // ADD -> UPDATE -> Undo
            trans.add(addTransaction);
            trans.add(updateTransaction);
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([addTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: addTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(32);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(33);

            // ADD -> UPDATE -> Undo -> Redo
            trans.add(addTransaction);
            trans.add(updateTransaction);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog()).toEqual([addTransaction, updateTransaction]);
            expect(trans.getState(addTransaction.id)).toEqual({
                value: updateTransaction.newValue,
                recordRef: undefined,
                type: addTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(37);

            trans.clear();
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(38);
        });

        it('Should add DELETE type transaction - all feasible paths', () => {
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            // DELETE
            const recordRef = { key: 'Key1', value: 1 };
            const deleteTransaction: Transaction = { id: 'Key1', type: TransactionType.DELETE, newValue: null };
            trans.add(deleteTransaction, recordRef);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);
            expect(trans.getTransactionLog()).toEqual([deleteTransaction]);
            expect(trans.getState(deleteTransaction.id)).toEqual({
                value: null,
                recordRef,
                type: deleteTransaction.type
            });
            trans.clear();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            // DELETE -> Undo
            trans.add(deleteTransaction, recordRef);
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            trans.clear();

            // DELETE -> Undo -> Redo
            trans.add(deleteTransaction, recordRef);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);
            expect(trans.getTransactionLog()).toEqual([deleteTransaction]);
            expect(trans.getState(deleteTransaction.id)).toEqual({
                value: null,
                recordRef,
                type: deleteTransaction.type
            });
            trans.clear();
        });

        it('Should add UPDATE type transaction - all feasible paths', () => {
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            // UPDATE
            const recordRef = { key: 'Key1', value: 1 };
            const newValue = { key: 'Key1', value: 2 };
            const updateTransaction: Transaction = { id: 'Key1', type: TransactionType.UPDATE, newValue };
            trans.add(updateTransaction, recordRef);
            expect(trans.getState('Key1')).toBeTruthy();
            expect(trans.getAggregatedValue('Key1', true)).toEqual(newValue);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 2 },
                recordRef,
                type: updateTransaction.type
            });
            trans.clear();
            expect(trans.getState('Key1')).toBeFalsy();
            expect(trans.getAggregatedValue('Key1', true)).toBeNull();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            // UPDATE -> Undo
            trans.add(updateTransaction, recordRef);
            trans.undo();
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            trans.clear();

            // UPDATE -> Undo -> Redo
            trans.add(updateTransaction, recordRef);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 2 },
                recordRef,
                type: updateTransaction.type
            });
            trans.clear();

            // UPDATE -> UPDATE
            trans.add(updateTransaction, recordRef);
            const newValue2 = { key: 'Key1', value: 3 };
            const updateTransaction2: Transaction = { id: 'Key1', type: TransactionType.UPDATE, newValue: newValue2 };
            trans.add(updateTransaction2, recordRef);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction2);
            expect(trans.getTransactionLog()).toEqual([updateTransaction, updateTransaction2]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 3 },
                recordRef,
                type: updateTransaction2.type
            });
            trans.clear();

            // UPDATE -> UPDATE (to initial recordRef)
            trans.add(updateTransaction, recordRef);
            const asRecordRefTransaction: Transaction = { id: 'Key1', type: TransactionType.UPDATE, newValue: recordRef };
            trans.add(asRecordRefTransaction, recordRef);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(asRecordRefTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction, asRecordRefTransaction]);
            expect(trans.getState(updateTransaction.id)).toBeUndefined();
            expect(trans.getAggregatedChanges(false)).toEqual([]);
            trans.clear();

            // UPDATE -> UPDATE -> Undo
            trans.add(updateTransaction, recordRef);
            trans.add(updateTransaction2, recordRef);
            trans.undo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 2 },
                recordRef,
                type: updateTransaction.type
            });
            trans.clear();

            // UPDATE -> UPDATE -> Undo -> Redo
            trans.add(updateTransaction, recordRef);
            trans.add(updateTransaction2, recordRef);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction2);
            expect(trans.getTransactionLog()).toEqual([updateTransaction, updateTransaction2]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 3 },
                recordRef,
                type: updateTransaction2.type
            });
            trans.clear();

            // UPDATE -> DELETE
            trans.add(updateTransaction, recordRef);
            const deleteTransaction: Transaction = { id: 'Key1', type: TransactionType.DELETE, newValue: null };
            trans.add(deleteTransaction);
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction, deleteTransaction]);
            expect(trans.getState(deleteTransaction.id)).toEqual({
                value: deleteTransaction.newValue,
                recordRef,
                type: deleteTransaction.type
            });
            trans.clear();

            // UPDATE -> DELETE -> Undo
            trans.add(updateTransaction, recordRef);
            trans.add(deleteTransaction);
            trans.undo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(updateTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value: 2 },
                recordRef,
                type: updateTransaction.type
            });
            trans.clear();

            // UPDATE -> DELETE -> Undo -> Redo
            trans.add(updateTransaction, recordRef);
            trans.add(deleteTransaction);
            trans.undo();
            trans.redo();
            expect(trans.getTransactionLog('Key1').pop()).toEqual(deleteTransaction);
            expect(trans.getTransactionLog()).toEqual([updateTransaction, deleteTransaction]);
            expect(trans.getState(deleteTransaction.id)).toEqual({
                value: deleteTransaction.newValue,
                recordRef,
                type: deleteTransaction.type
            });
            trans.clear();
        });

        it('Should properly confirm the length of the undo/redo stacks', () => {
            const transaction = new IgxTransactionService();
            expect(transaction).toBeDefined();
            // Stacks are clear by default
            expect(transaction.canRedo).toBeFalsy();
            expect(transaction.canUndo).toBeFalsy();
            let addItem: Transaction = { id: 1, type: TransactionType.ADD, newValue: { Category: 'Something' } };
            transaction.add(addItem);
            expect(transaction.canRedo).toBeFalsy();
            expect(transaction.canUndo).toBeTruthy();
            addItem = { id: 2, type: TransactionType.ADD, newValue: { Category: 'Something 2' } };
            transaction.add(addItem);
            expect(transaction.canRedo).toBeFalsy();
            expect(transaction.canUndo).toBeTruthy();
            transaction.undo();
            expect(transaction.canRedo).toBeTruthy();
            expect(transaction.canUndo).toBeTruthy();
            transaction.undo();
            expect(transaction.canRedo).toBeTruthy();
            expect(transaction.canUndo).toBeFalsy();
            transaction.redo();
            expect(transaction.canRedo).toBeTruthy();
            expect(transaction.canUndo).toBeTruthy();
            transaction.redo();
            expect(transaction.canRedo).toBeFalsy();
            expect(transaction.canUndo).toBeTruthy();
        });

        it('Should update data when data is list of objects', () => {
            const originalData = SampleTestData.generateProductData(50);
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            const item0Update1: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            trans.add(item0Update1, originalData[1]);

            const item10Delete: Transaction = { id: 10, type: TransactionType.DELETE, newValue: null };
            trans.add(item10Delete, originalData[10]);

            const newItem1: Transaction = {
                id: 'add1', type: TransactionType.ADD, newValue: {
                    ID: undefined,
                    Category: 'Category Added',
                    Downloads: 100,
                    Items: 'Items Added',
                    ProductName: 'ProductName Added',
                    ReleaseDate: new Date(),
                    Released: true,
                    Test: 'test Added'
                }
            };

            trans.add(newItem1, undefined);

            trans.commit(originalData);
            expect(originalData.find(i => i.ID === 1).Category).toBe('Some new value');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(50);
            expect(originalData[49]).toEqual(newItem1.newValue);
        });

        it('Should update data for provided id when data is list of objects', () => {
            const originalData = SampleTestData.generateProductData(50);
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            const item0Update1: Transaction = { id: 0, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            trans.add(item0Update1, originalData[1]);

            const item10Delete: Transaction = { id: 10, type: TransactionType.DELETE, newValue: null };
            trans.add(item10Delete, originalData[10]);

            const newItem1: Transaction = {
                id: 'add1', type: TransactionType.ADD, newValue: {
                    ID: undefined,
                    Category: 'Category Added',
                    Downloads: 100,
                    Items: 'Items Added',
                    ProductName: 'ProductName Added',
                    ReleaseDate: new Date(),
                    Released: true,
                    Test: 'test Added'
                }
            };

            trans.add(newItem1, undefined);

            trans.commit(originalData, 10);
            expect(originalData.find(i => i.ID === 1).Category).toBe('Category1');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 'FAKE ID');
            expect(originalData.find(i => i.ID === 1).Category).toBe('Category1');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 20);
            expect(originalData.find(i => i.ID === 1).Category).toBe('Category1');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 0);
            expect(originalData.find(i => i.ID === 1).Category).toBe('Some new value');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 'add1');
            expect(originalData.find(i => i.ID === 1).Category).toBe('Some new value');
            expect(originalData.find(i => i.ID === 10)).toBeUndefined();
            expect(originalData.length).toBe(50);
            expect(originalData[49]).toEqual(newItem1.newValue);
        });

        it('Should update data when data is list of primitives', () => {
            const originalData = SampleTestData.generateListOfPrimitiveValues(50, 'String');
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            const item0Update1: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Updated Row' };
            trans.add(item0Update1, originalData[1]);

            const item10Delete: Transaction = { id: 10, type: TransactionType.DELETE, newValue: null };
            trans.add(item10Delete, originalData[10]);

            const newItem1: Transaction = {
                id: 'add1', type: TransactionType.ADD, newValue: 'Added Row'
            };

            trans.add(newItem1, undefined);

            trans.commit(originalData);
            expect(originalData[1]).toBe('Updated Row');
            expect(originalData.find(i => i === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(50);
            expect(originalData[49]).toEqual('Added Row');
        });

        it('Should update data for provided id when data is list of primitives', () => {
            const originalData = SampleTestData.generateListOfPrimitiveValues(50, 'String');
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            const item0Update1: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Updated Row' };
            trans.add(item0Update1, originalData[1]);

            const item10Delete: Transaction = { id: 10, type: TransactionType.DELETE, newValue: null };
            trans.add(item10Delete, originalData[10]);

            const newItem1: Transaction = {
                id: 'add1', type: TransactionType.ADD, newValue: 'Added Row'
            };

            trans.add(newItem1, undefined);

            trans.commit(originalData, 10);
            expect(originalData[1]).toBe('Row 1');
            expect(originalData.find(i => i.id === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 'FAKE ID');
            expect(originalData[1]).toBe('Row 1');
            expect(originalData.find(i => i.id === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 20);
            expect(originalData[1]).toBe('Row 1');
            expect(originalData.find(i => i.id === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 1);
            expect(originalData[1]).toBe('Updated Row');
            expect(originalData.find(i => i.id === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(49);

            trans.commit(originalData, 'add1');
            expect(originalData[1]).toBe('Updated Row');
            expect(originalData.find(i => i.id === 'Row 10')).toBeUndefined();
            expect(originalData.length).toBe(50);
            expect(originalData[49]).toEqual(newItem1.newValue);
        });

        it('Should add pending transaction and push it to transaction log, and correctly fires onStateUpdate', () => {
            const trans = new IgxTransactionService();
            spyOn(trans.onStateUpdate, 'emit').and.callThrough();

            expect(trans).toBeDefined();
            const recordRef = { key: 'Key1', value1: 1, value2: 2, value3: 3 };
            let newValue: any = { key: 'Key1', value1: 10 };
            let updateTransaction: Transaction = { id: 'Key1', type: TransactionType.UPDATE, newValue };

            trans.startPending();
            trans.add(updateTransaction, recordRef);

            expect(trans.getState('Key1')).toBeUndefined();
            expect(trans.getAggregatedValue('Key1', true)).toEqual({ key: 'Key1', value1: 10, value2: 2, value3: 3 });
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            newValue = { key: 'Key1', value3: 30 };
            updateTransaction = { id: 'Key1', type: TransactionType.UPDATE, newValue };
            trans.add(updateTransaction, recordRef);

            expect(trans.getState('Key1')).toBeUndefined();
            expect(trans.getAggregatedValue('Key1', true)).toEqual({ key: 'Key1', value1: 10, value2: 2, value3: 30 });
            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            trans.endPending(true);

            expect(trans.getState('Key1')).toBeTruthy();
            expect(trans.getAggregatedValue('Key1', true)).toEqual({ key: 'Key1', value1: 10, value2: 2, value3: 30 });
            expect(trans.getTransactionLog() as any).toEqual(
                [
                    {
                        id: 'Key1',
                        newValue: { key: 'Key1', value1: 10 },
                        type: 'update'
                    }, {
                        id: 'Key1',
                        newValue: { key: 'Key1', value3: 30 },
                        type: 'update'
                    }
                ]);
            expect(trans.getState(updateTransaction.id)).toEqual({
                value: { value1: 10, value3: 30 },
                recordRef,
                type: updateTransaction.type
            });
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(1);
        });

        it('Should not add pending transaction and push it to transaction log, and correctly fires onStateUpdate', () => {
            const trans = new IgxTransactionService();
            spyOn(trans.onStateUpdate, 'emit').and.callThrough();

            expect(trans).toBeDefined();
            const recordRef = { key: 'Key1', value1: 1, value2: 2, value3: 3 };
            let newValue: any = { key: 'Key1', value1: 10 };
            let updateTransaction: Transaction = { id: 'Key1', type: TransactionType.UPDATE, newValue };

            trans.startPending();
            trans.add(updateTransaction, recordRef);

            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            newValue = { key: 'Key1', value3: 30 };
            updateTransaction = { id: 'Key1', type: TransactionType.UPDATE, newValue };
            trans.add(updateTransaction, recordRef);

            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);

            trans.endPending(false);

            expect(trans.getTransactionLog()).toEqual([]);
            expect(trans.getAggregatedChanges(true)).toEqual([]);
            expect(trans.onStateUpdate.emit).toHaveBeenCalledTimes(0);
        });

        it('Should not generate changes when updating a value to the original one', () => {
            const originalData = SampleTestData.generateProductData(50);
            const transaction = new IgxTransactionService();
            expect(transaction).toBeDefined();

            transaction.startPending();

            const itemUpdate1: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            transaction.add(itemUpdate1, originalData[1]);

            expect(transaction.getState(1, true)).toBeTruthy();
            expect(transaction.getAggregatedValue(1, false)).toEqual({ Category: 'Some new value' });

            // update to original value
            const itemUpdate2: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: { Category: originalData[1].Category } };
            transaction.add(itemUpdate2, originalData[1]);

            expect(transaction.getState(1, true)).toBeUndefined();
            expect(transaction.getAggregatedValue(1, false)).toBeNull();

            transaction.endPending(false);

            expect(transaction.getTransactionLog()).toEqual([]);
            expect(transaction.getAggregatedChanges(true)).toEqual([]);
        });

        it('Should clear transactions for provided id', () => {
            const originalData = SampleTestData.generateProductData(50);
            const trans = new IgxTransactionService();
            expect(trans).toBeDefined();

            let transaction: Transaction = { id: 1, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            trans.add(transaction, originalData[1]);

            transaction = { id: 2, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            trans.add(transaction, originalData[2]);

            transaction = { id: 2, type: TransactionType.UPDATE, newValue: { Items: 'Some new value' } };
            trans.add(transaction, originalData[2]);

            transaction = { id: 1, type: TransactionType.UPDATE, newValue: { Category: 'Some very new value' } };
            trans.add(transaction, originalData[1]);

            transaction = { id: 10, type: TransactionType.UPDATE, newValue: { Category: 'Some new value' } };
            trans.add(transaction, originalData[10]);

            expect(trans.getTransactionLog().length).toBe(5);
            expect(trans.getAggregatedChanges(true).length).toBe(3);
            expect(trans.canUndo).toBeTruthy();
            expect(trans.canRedo).toBeFalsy();

            trans.clear(1);
            expect(trans.getTransactionLog().length).toBe(3);
            expect(trans.getAggregatedChanges(true).length).toBe(2);
            expect(trans.canUndo).toBeTruthy();
            expect(trans.canRedo).toBeFalsy();

            trans.clear('FAKE ID');
            expect(trans.getTransactionLog().length).toBe(3);
            expect(trans.getAggregatedChanges(true).length).toBe(2);
            expect(trans.canUndo).toBeTruthy();
            expect(trans.canRedo).toBeFalsy();

            trans.clear(20);
            expect(trans.getTransactionLog().length).toBe(3);
            expect(trans.getAggregatedChanges(true).length).toBe(2);
            expect(trans.canUndo).toBeTruthy();
            expect(trans.canRedo).toBeFalsy();

            trans.clear(10);
            expect(trans.getTransactionLog().length).toBe(2);
            expect(trans.getAggregatedChanges(true).length).toBe(1);
            expect(trans.canUndo).toBeTruthy();
            expect(trans.canRedo).toBeFalsy();
        });
    });

    describe('IgxHierarchicalTransaction UNIT Test', () => {
        it('Should set path for each state when transaction is added in Hierarchical data source', () => {
            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            const path: any[] = ['P1', 'P2'];
            const addTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.ADD, newValue: 'Add row', path };
            transaction.add(addTransaction);
            expect(transaction.getState(1).path).toBeDefined();
            expect(transaction.getState(1).path.length).toBe(2);
            expect(transaction.getState(1).path).toEqual(path);

            path.push('P3');
            const updateTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Updated row', path };
            transaction.add(updateTransaction, 'Update row');
            expect(transaction.getState(1).path.length).toBe(3);
            expect(transaction.getState(1).path).toEqual(path);
        });

        it('Should remove added transaction from states when deleted in Hierarchical data source', () => {
            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            const path: any[] = [];
            let addTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.ADD, newValue: 'Parent row', path };
            transaction.add(addTransaction);
            expect(transaction.getState(1).path).toBeDefined();
            expect(transaction.getState(1).path.length).toBe(0);
            expect(transaction.getState(1).path).toEqual(path);

            path.push(addTransaction.id);
            addTransaction = { id: 2, type: TransactionType.ADD, newValue: 'Child row', path };
            transaction.add(addTransaction);
            expect(transaction.getState(2).path).toBeDefined();
            expect(transaction.getState(2).path.length).toBe(1);
            expect(transaction.getState(2).path).toEqual(path);

            const deleteTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.DELETE, newValue: null, path: [] };
            transaction.add(deleteTransaction);
            expect(transaction.getState(1)).toBeUndefined();
            expect(transaction.getState(2)).toBeUndefined();
        });

        it('Should mark update transactions state as deleted type when deleted in Hierarchical data source', () => {
            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            const path: any[] = [];
            let updateTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Parent row', path };
            transaction.add(updateTransaction, 'Original value');
            expect(transaction.getState(1).path).toBeDefined();
            expect(transaction.getState(1).path.length).toBe(0);
            expect(transaction.getState(1).path).toEqual(path);

            path.push(updateTransaction.id);
            updateTransaction = { id: 2, type: TransactionType.UPDATE, newValue: 'Child row', path };
            transaction.add(updateTransaction, 'Original Value');
            expect(transaction.getState(2).path).toBeDefined();
            expect(transaction.getState(2).path.length).toBe(1);
            expect(transaction.getState(2).path).toEqual(path);

            const deleteTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.DELETE, newValue: null, path: [] };
            transaction.add(deleteTransaction);
            expect(transaction.getState(1)).toBeDefined();
            expect(transaction.getState(1).type).toBe(TransactionType.DELETE);
            expect(transaction.getState(2)).toBeDefined();
            expect(transaction.getState(2).type).toBe(TransactionType.DELETE);
        });

        it('Should correctly call getAggregatedChanges without commit when recordRef is null', () => {
            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            const deleteTransaction: HierarchicalTransaction = { id: 0, type: TransactionType.DELETE, newValue: null, path: [] };
            transaction.add(deleteTransaction, 'Deleted row');

            expect(transaction.getAggregatedChanges(false)).toEqual([deleteTransaction]);
        });

        it('Should update data for provided id', () => {
            const data = SampleTestData.employeeTreeData();

            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            const addTransaction: HierarchicalTransaction = {
                id: 0,
                type: TransactionType.ADD,
                newValue: {
                    ID: 999,
                    Name: 'Root Add Transaction',
                    HireDate: new Date(2018, 3, 20),
                    Age: 45,
                    OnPTO: false,
                    Employees: []
                },
                path: null
            };
            transaction.add(addTransaction);

            const updateTransaction: HierarchicalTransaction = {
                id: 475,
                type: TransactionType.UPDATE,
                newValue: {
                    Age: 60
                },
                path: [data[0].ID]
            };
            transaction.add(updateTransaction, data[0].Employees[0]);

            const deleteTransaction: HierarchicalTransaction = {
                id: 711,
                type: TransactionType.DELETE,
                newValue: {},
                path: [data[0].ID, data[0].Employees[2].ID]
            };
            transaction.add(deleteTransaction, data[0].Employees[2].Employees[0]);

            updateTransaction.newValue = { Name: 'New Name'};
            transaction.add(updateTransaction, data[0].Employees[0]);

            expect(data.find(i => i.ID === 999)).toBeUndefined();
            expect(data.length).toBe(4);
            transaction.commit(data, 'ID', 'Employees', 0);
            expect(data.find(i => i.ID === 999)).toBeDefined();
            expect(data.find(i => i.ID === 999).Name).toBe('Root Add Transaction');
            expect(data.length).toBe(5);
            expect(transaction.canUndo).toBeTruthy();
            expect(transaction.getAggregatedChanges(false).length).toBe(2);

            expect(data[0].Employees[0].Age).toBe(43);
            expect(data[0].Employees[0].Name).toBe('Michael Langdon');
            transaction.commit(data, 'ID', 'Employees', 475);
            expect(data[0].Employees[0].Age).toBe(60);
            expect(data[0].Employees[0].Name).toBe('New Name');
            expect(transaction.canUndo).toBeTruthy();
            expect(transaction.getAggregatedChanges(false).length).toBe(1);

            expect(data[0].Employees[2].Employees.length).toBe(2);
            transaction.commit(data, 'ID', 'Employees', 711);
            expect(data[0].Employees[2].Employees.length).toBe(1);
            expect(transaction.canUndo).toBeFalsy();
            expect(transaction.getAggregatedChanges(false).length).toBe(0);
        });

        it('Should not generate changes when updating a value to the original one', () => {
            const originalData = SampleTestData.employeeTreeData();
            const transaction = new IgxHierarchicalTransactionService();
            expect(transaction).toBeDefined();

            transaction.startPending();

            // root record update
            const rootUpdate1: HierarchicalTransaction = {
                id: 147,
                type: TransactionType.UPDATE,
                newValue: {
                    Name: 'New Name'
                },
                path: null
            };
            transaction.add(rootUpdate1, originalData[0]);

            expect(transaction.getState(147, true)).toBeTruthy();
            expect(transaction.getAggregatedValue(147, false)).toEqual({ Name: 'New Name' });

            // update to original value
            const rootUpdate2: HierarchicalTransaction = {
                id: 147,
                type: TransactionType.UPDATE,
                newValue: {
                    Name: originalData[0].Name
                },
                path: null
            };
            transaction.add(rootUpdate2, originalData[0]);

            expect(transaction.getState(147, true)).toBeUndefined();
            expect(transaction.getAggregatedValue(147, false)).toBeNull();

            // child record update
            const childUpdate1: HierarchicalTransaction = {
                id: 475,
                type: TransactionType.UPDATE,
                newValue: {
                    Age: 60
                },
                path: [originalData[0].ID]
            };
            transaction.add(childUpdate1, originalData[0].Employees[0]);

            expect(transaction.getState(475, true)).toBeTruthy();
            expect(transaction.getAggregatedValue(475, false)).toEqual({ Age: 60 });

            // update to original value
            const childUpdate2: HierarchicalTransaction = {
                id: 475,
                type: TransactionType.UPDATE,
                newValue: {
                    Age: originalData[0].Employees[0].Age
                },
                path: [originalData[0].ID]
            };
            transaction.add(childUpdate2, originalData[0].Employees[0]);

            expect(transaction.getState(475, true)).toBeUndefined();
            expect(transaction.getAggregatedValue(475, false)).toBeNull();

            transaction.endPending(false);

            expect(transaction.getTransactionLog()).toEqual([]);
            expect(transaction.getAggregatedChanges(true)).toEqual([]);
        });

        it('Should emit onStateUpdate once when commiting a hierarchical transaction', () => {
            const data = SampleTestData.employeeTreeData();
            const transaction = new IgxHierarchicalTransactionService();
            spyOn(transaction.onStateUpdate, 'emit').and.callThrough();
            expect(transaction).toBeDefined();

            const updateTransaction: HierarchicalTransaction = {
                id: 475,
                type: TransactionType.UPDATE,
                newValue: {
                    Age: 60
                },
                path: [data[0].ID]
            };
            transaction.add(updateTransaction, data[0].Employees[0]);
            expect(transaction.onStateUpdate.emit).toHaveBeenCalledTimes(1);

            transaction.commit(data, 'ID');
            expect(transaction.onStateUpdate.emit).toHaveBeenCalledTimes(2);
        });
    });
});
