import { NgTemplateOutlet } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { StoreModule } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core';
import {
  cold,
  getTestScheduler,
} from 'jasmine-marbles';
import {
  BehaviorSubject,
  of as observableOf,
} from 'rxjs';

import { storeModuleConfig } from '../app.reducer';
import { authReducer } from '../core/auth/auth.reducer';
import { AuthService } from '../core/auth/auth.service';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { RestResponse } from '../core/cache/response.models';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { EPersonDataService } from '../core/eperson/eperson-data.service';
import { EPerson } from '../core/eperson/models/eperson.model';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { SuggestionsNotificationComponent } from '../notifications/suggestions-notification/suggestions-notification.component';
import { ErrorComponent } from '../shared/error/error.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { PaginationComponent } from '../shared/pagination/pagination.component';
import {
  createFailedRemoteDataObject$,
  createSuccessfulRemoteDataObject$,
} from '../shared/remote-data.utils';
import {
  EmptySpecialGroupDataMock$,
  SpecialGroupDataMock$,
} from '../shared/testing/special-group.mock';
import { createPaginatedList } from '../shared/testing/utils.test';
import { VarDirective } from '../shared/utils/var.directive';
import { ProfilePageComponent } from './profile-page.component';
import { ThemedProfilePageMetadataFormComponent } from './profile-page-metadata-form/themed-profile-page-metadata-form.component';
import { ProfilePageResearcherFormComponent } from './profile-page-researcher-form/profile-page-researcher-form.component';
import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';

describe('ProfilePageComponent', () => {
  let component: ProfilePageComponent;
  let fixture: ComponentFixture<ProfilePageComponent>;
  let user;
  let initialState: any;

  let authService;
  let authorizationService;
  let epersonService;
  let notificationsService;
  let configurationService;

  const canChangePassword = new BehaviorSubject(true);
  const validConfiguration = Object.assign(new ConfigurationProperty(), {
    name: 'researcher-profile.entity-type',
    values: [
      'Person',
    ],
  });
  const emptyConfiguration = Object.assign(new ConfigurationProperty(), {
    name: 'researcher-profile.entity-type',
    values: [],
  });

  function init() {
    user = Object.assign(new EPerson(), {
      id: 'userId',
      groups: createSuccessfulRemoteDataObject$(createPaginatedList([])),
      _links: { self: { href: 'test.com/uuid/1234567654321' } },
    });
    initialState = {
      core: {
        auth: {
          authenticated: true,
          loaded: true,
          blocking: false,
          loading: false,
          authToken: new AuthTokenInfo('test_token'),
          userId: user.id,
          authMethods: [],
        },
      },
    };
    authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword });
    authService = jasmine.createSpyObj('authService', {
      getAuthenticatedUserFromStore: observableOf(user),
      getSpecialGroupsFromAuthStatus: SpecialGroupDataMock$,
    });
    epersonService = jasmine.createSpyObj('epersonService', {
      findById: createSuccessfulRemoteDataObject$(user),
      patch: observableOf(Object.assign(new RestResponse(true, 200, 'Success'))),
    });
    notificationsService = jasmine.createSpyObj('notificationsService', {
      success: {},
      error: {},
      warning: {},
    });
    configurationService = jasmine.createSpyObj('configurationDataService', {
      findByPropertyName: jasmine.createSpy('findByPropertyName'),
    });
  }

  beforeEach(waitForAsync(() => {
    init();
    TestBed.configureTestingModule({
      imports: [
        StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
        TranslateModule.forRoot(),
        RouterModule.forRoot([]),
        ProfilePageComponent,
        VarDirective,
      ],
      providers: [
        { provide: EPersonDataService, useValue: epersonService },
        { provide: NotificationsService, useValue: notificationsService },
        { provide: AuthService, useValue: authService },
        { provide: ConfigurationDataService, useValue: configurationService },
        { provide: AuthorizationDataService, useValue: authorizationService },
        provideMockStore({ initialState }),
      ],
      schemas: [NO_ERRORS_SCHEMA],
    })
      .overrideComponent(ProfilePageComponent, {
        remove: {
          imports: [
            ThemedProfilePageMetadataFormComponent,
            ProfilePageSecurityFormComponent,
            ProfilePageResearcherFormComponent,
            SuggestionsNotificationComponent,
            NgTemplateOutlet,
            PaginationComponent,
            ThemedLoadingComponent,
            ErrorComponent,
          ],
        },
      })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ProfilePageComponent);
    component = fixture.componentInstance;
  });

  describe('', () => {

    beforeEach(() => {
      configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration));
      fixture.detectChanges();
    });

    describe('updateProfile', () => {
      describe('when the metadata form returns false and the security form returns true', () => {
        beforeEach(() => {
          component.metadataForm = {
            compRef: {
              instance: {
                updateProfile: () => false,
              },
            },
          } as any;
          spyOn(component, 'updateSecurity').and.returnValue(true);
          component.updateProfile();
        });

        it('should not display a warning', () => {
          expect(notificationsService.warning).not.toHaveBeenCalled();
        });
      });

      describe('when the metadata form returns true and the security form returns false', () => {
        beforeEach(() => {
          component.metadataForm = {
            compRef: {
              instance: {
                updateProfile: () => true,
              },
            },
          } as any;
          component.updateProfile();
        });

        it('should not display a warning', () => {
          expect(notificationsService.warning).not.toHaveBeenCalled();
        });
      });

      describe('when the metadata form returns true and the security form returns true', () => {
        beforeEach(() => {
          component.metadataForm = {
            compRef: {
              instance: {
                updateProfile: () => true,
              },
            },
          } as any;
          component.updateProfile();
        });

        it('should not display a warning', () => {
          expect(notificationsService.warning).not.toHaveBeenCalled();
        });
      });

      describe('when the metadata form returns false and the security form returns false', () => {
        beforeEach(() => {
          component.metadataForm = {
            compRef: {
              instance: {
                updateProfile: () => false,
              },
            },
          } as any;
          component.updateProfile();
        });

        it('should display a warning', () => {
          expect(notificationsService.warning).toHaveBeenCalled();
        });
      });
    });

    describe('updateSecurity', () => {
      describe('when no password value present', () => {
        let result;

        beforeEach(() => {
          component.setPasswordValue('');
          component.setCurrentPasswordValue('current-password');
          result = component.updateSecurity();
        });

        it('should return false', () => {
          expect(result).toEqual(false);
        });

        it('should not call epersonService.patch', () => {
          expect(epersonService.patch).not.toHaveBeenCalled();
        });
      });

      describe('when password is filled in, but the password is invalid', () => {
        let result;

        beforeEach(() => {
          component.setPasswordValue('test');
          component.setInvalid(true);
          component.setCurrentPasswordValue('current-password');
          result = component.updateSecurity();
        });

        it('should return true', () => {
          expect(result).toEqual(true);
          expect(epersonService.patch).not.toHaveBeenCalled();
        });
      });

      describe('when password is filled in, and is valid', () => {
        let result;
        let operations;

        beforeEach(() => {
          component.setPasswordValue('testest');
          component.setInvalid(false);
          component.setCurrentPasswordValue('current-password');

          operations = [
            { 'op': 'add', 'path': '/password', 'value': { 'new_password': 'testest', 'current_password': 'current-password' } },
          ];
          result = component.updateSecurity();
        });

        it('should return true', () => {
          expect(result).toEqual(true);
        });

        it('should return call epersonService.patch', () => {
          expect(epersonService.patch).toHaveBeenCalledWith(user, operations);
        });
      });

      describe('when password is filled in, and is valid but return 403', () => {
        let result;
        let operations;

        it('should return call epersonService.patch', (done) => {
          epersonService.patch.and.returnValue(observableOf(Object.assign(new RestResponse(false, 403, 'Error'))));
          component.setPasswordValue('testest');
          component.setInvalid(false);
          component.setCurrentPasswordValue('current-password');
          operations = [
            { 'op': 'add', 'path': '/password', 'value': { 'new_password': 'testest', 'current_password': 'current-password'  } },
          ];
          result = component.updateSecurity();
          epersonService.patch(user, operations).subscribe((response) => {
            expect(response.statusCode).toEqual(403);
            done();
          });
          expect(epersonService.patch).toHaveBeenCalledWith(user, operations);
          expect(result).toEqual(true);
        });
      });
    });

    describe('canChangePassword$', () => {
      describe('when the user is allowed to change their password', () => {
        beforeEach(() => {
          canChangePassword.next(true);
        });

        it('should contain true', () => {
          getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true });
        });

        it('should show the security section on the page', () => {
          fixture.detectChanges();
          expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull();
        });
      });

      describe('when the user is not allowed to change their password', () => {
        beforeEach(() => {
          canChangePassword.next(false);
        });

        it('should contain false', () => {
          getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false });
        });

        it('should not show the security section on the page', () => {
          fixture.detectChanges();
          expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull();
        });
      });
    });

    describe('check for specialGroups', () => {
      it('should contains specialGroups list', () => {
        const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
        expect(specialGroupsEle).toBeTruthy();
      });

      it('should not contains specialGroups list', () => {
        component.specialGroupsRD$ = null;
        fixture.detectChanges();
        const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
        expect(specialGroupsEle).toBeFalsy();
      });

      it('should not contains specialGroups list', () => {
        component.specialGroupsRD$ = EmptySpecialGroupDataMock$;
        fixture.detectChanges();
        const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
        expect(specialGroupsEle).toBeFalsy();
      });
    });
  });

  describe('isResearcherProfileEnabled', () => {

    describe('when configuration service return values', () => {

      beforeEach(() => {
        configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration));
        fixture.detectChanges();
      });

      it('should return true', () => {
        const result = component.isResearcherProfileEnabled$;
        const expected = cold('a', {
          a: true,
        });
        expect(result).toBeObservable(expected);
      });
    });

    describe('when configuration service return no values', () => {

      beforeEach(() => {
        configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(emptyConfiguration));
        fixture.detectChanges();
      });

      it('should return false', () => {
        const result = component.isResearcherProfileEnabled$;
        const expected = cold('a', {
          a: false,
        });
        expect(result).toBeObservable(expected);
      });
    });

    describe('when configuration service return an error', () => {

      beforeEach(() => {
        configurationService.findByPropertyName.and.returnValue(createFailedRemoteDataObject$());
        fixture.detectChanges();
      });

      it('should return false', () => {
        const result = component.isResearcherProfileEnabled$;
        const expected = cold('a', {
          a: false,
        });
        expect(result).toBeObservable(expected);
      });
    });
  });
});