import * as deepFreeze from 'deep-freeze';

import {
  CommitPatchOperationsAction,
  FlushPatchOperationsAction,
  NewPatchAddOperationAction,
  NewPatchRemoveOperationAction,
  RollbacktPatchOperationsAction,
  StartTransactionPatchOperationsAction
} from './json-patch-operations.actions';
import {
  JsonPatchOperationsEntry,
  jsonPatchOperationsReducer,
  JsonPatchOperationsResourceEntry,
  JsonPatchOperationsState
} from './json-patch-operations.reducer';

class NullAction extends NewPatchAddOperationAction {
  resourceType: string;
  resourceId: string;
  path: string;
  value: any;

  constructor() {
    super(null, null, null, null);
    this.type = null;
  }
}

describe('jsonPatchOperationsReducer test suite', () => {
  const testJsonPatchResourceType = 'testResourceType';
  const testJsonPatchResourceId = 'testResourceId';
  const testJsonPatchResourceAnotherId = 'testResourceAnotherId';
  const testJsonPatchResourcePath = '/testResourceType/testResourceId/testField';
  const testJsonPatchResourceValue = ['test'];
  const patchOpBody = [{
    op: 'add',
    path: '/testResourceType/testResourceId/testField',
    value: ['test']
  }];
  const timestampBeforeStart = 1545994811991;
  const timestampAfterStart = 1545994837492;
  const startTimestamp = 1545994827492;
  const testState: JsonPatchOperationsState = {
        testResourceType: {
          children: {
            testResourceId: {
              body: [
                {
                  operation: {
                    op: 'add',
                    path: '/testResourceType/testResourceId/testField',
                    value: ['test']
                  },
                  timeAdded: timestampBeforeStart
                },
              ]
            } as JsonPatchOperationsEntry
          },
          transactionStartTime: null,
          commitPending: false
        } as JsonPatchOperationsResourceEntry
    };

  let initState: JsonPatchOperationsState;

  const anotherTestState: JsonPatchOperationsState = {
    testResourceType: {
      children: {
        testResourceId: {
          body: [
            {
              operation: {
                op: 'add',
                path: '/testResourceType/testResourceId/testField',
                value: ['test']
              },
              timeAdded: timestampBeforeStart
            },
            {
              operation: {
                op: 'remove',
                path: '/testResourceType/testResourceId/testField'
              },
              timeAdded: timestampBeforeStart
            },
          ]
        } as JsonPatchOperationsEntry
      },
      transactionStartTime: null,
      commitPending: false
    } as JsonPatchOperationsResourceEntry
  };
  deepFreeze(testState);

  beforeEach(() => {
    spyOn(Date.prototype, 'getTime').and.callFake(() => {
      return timestampBeforeStart;
    });
  });

  it('should start with an empty state', () => {
    const action = new NullAction();
    const initialState = jsonPatchOperationsReducer(undefined, action);

    expect(initialState).toEqual(Object.create(null));
  });

  it('should return the current state when no valid actions have been made', () => {
    const action = new NullAction();
    const newState = jsonPatchOperationsReducer(testState, action);

    expect(newState).toEqual(testState);
  });

  describe('When a new patch operation actions have been dispatched', () => {

    it('should return the properly state when it is empty', () => {
      const action = new NewPatchAddOperationAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId,
        testJsonPatchResourcePath,
        testJsonPatchResourceValue);
      const newState = jsonPatchOperationsReducer(undefined, action);

      expect(newState).toEqual(testState);
    });

    it('should return the properly state when it is not empty', () => {
      const action = new NewPatchRemoveOperationAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId,
        testJsonPatchResourcePath);
      const newState = jsonPatchOperationsReducer(testState, action);

      expect(newState).toEqual(anotherTestState);
    });
  });

  describe('When StartTransactionPatchOperationsAction has been dispatched', () => {
    it('should set \'transactionStartTime\' and \'commitPending\' to true', () => {
      const action = new StartTransactionPatchOperationsAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId,
        startTimestamp);
      const newState = jsonPatchOperationsReducer(testState, action);

      expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp);
      expect(newState[testJsonPatchResourceType].commitPending).toBeTruthy();
    });
  });

  describe('When CommitPatchOperationsAction has been dispatched', () => {
    it('should set \'commitPending\' to false ', () => {
      const action = new CommitPatchOperationsAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId);
      initState = Object.assign({}, testState, {
        [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
          transactionStartTime: startTimestamp,
          commitPending: true
        })
      });
      const newState = jsonPatchOperationsReducer(initState, action);

      expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp);
      expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
    });
  });

  describe('When RollbacktPatchOperationsAction has been dispatched', () => {
    it('should set \'transactionStartTime\' to null and \'commitPending\' to false ', () => {
      const action = new RollbacktPatchOperationsAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId);
      initState = Object.assign({}, testState, {
        [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
          transactionStartTime: startTimestamp,
          commitPending: true
        })
      });
      const newState = jsonPatchOperationsReducer(initState, action);

      expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
      expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
    });
  });

  describe('When FlushPatchOperationsAction has been dispatched', () => {

    it('should flush only committed operations', () => {
      const action = new FlushPatchOperationsAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId);
      initState = Object.assign({}, testState, {
        [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
          children: {
            testResourceId: {
              body: [
                {
                  operation: {
                    op: 'add',
                    path: '/testResourceType/testResourceId/testField',
                    value: ['test']
                  },
                  timeAdded: timestampBeforeStart
                },
                {
                  operation: {
                    op: 'remove',
                    path: '/testResourceType/testResourceId/testField'
                  },
                  timeAdded: timestampAfterStart
                },
              ]
            } as JsonPatchOperationsEntry
          },
          transactionStartTime: startTimestamp,
          commitPending: false
        })
      });
      const newState = jsonPatchOperationsReducer(initState, action);
      const expectedBody: any = [
        {
          operation: {
            op: 'remove',
            path: '/testResourceType/testResourceId/testField'
          },
          timeAdded: timestampAfterStart
        },
      ];
      expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
      expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
      expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual(expectedBody);
    });

    beforeEach(() => {
      initState = Object.assign({}, testState, {
        [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
          children: {
            testResourceId: {
              body: [
                {
                  operation: {
                    op: 'add',
                    path: '/testResourceType/testResourceId/testField',
                    value: ['test']
                  },
                  timeAdded: timestampBeforeStart
                },
                {
                  operation: {
                    op: 'remove',
                    path: '/testResourceType/testResourceId/testField'
                  },
                  timeAdded: timestampBeforeStart
                },
              ]
            } as JsonPatchOperationsEntry,
            testResourceAnotherId: {
              body: [
                {
                  operation: {
                    op: 'add',
                    path: '/testResourceType/testResourceAnotherId/testField',
                    value: ['test']
                  },
                  timeAdded: timestampBeforeStart
                },
                {
                  operation: {
                    op: 'remove',
                    path: '/testResourceType/testResourceAnotherId/testField'
                  },
                  timeAdded: timestampBeforeStart
                },
              ]
            } as JsonPatchOperationsEntry
          },
          transactionStartTime: startTimestamp,
          commitPending: false
        })
      });
    });

    it('should flush committed operations for specified resource id', () => {
      const action = new FlushPatchOperationsAction(
        testJsonPatchResourceType,
        testJsonPatchResourceId);
      const newState = jsonPatchOperationsReducer(initState, action);
      const expectedBody: any = [
        {
          operation: {
            op: 'add',
            path: '/testResourceType/testResourceAnotherId/testField',
            value: ['test']
          },
          timeAdded: timestampBeforeStart
        },
        {
          operation: {
            op: 'remove',
            path: '/testResourceType/testResourceAnotherId/testField'
          },
          timeAdded: timestampBeforeStart
        },
      ];
      expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
      expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
      expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]);
      expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual(expectedBody);
    });

    it('should flush operation list', () => {
      const action = new FlushPatchOperationsAction(testJsonPatchResourceType, undefined);
      const newState = jsonPatchOperationsReducer(initState, action);

      expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
      expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
      expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]);
      expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual([]);
    });

  });

});
