jest.mock('@angular/router');
jest.mock('@ngx-translate/core');
jest.mock('app/core/auth/state-storage.service');
jest.mock('app/core/tracker/tracker.service');

import { Router } from '@angular/router';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { TranslateService } from '@ngx-translate/core';
import { NgxWebstorageModule, SessionStorageService } from 'ngx-webstorage';

import { Account } from 'app/core/auth/account.model';
import { Authority } from 'app/config/authority.constants';
import { StateStorageService } from 'app/core/auth/state-storage.service';
import { TrackerService } from 'app/core/tracker/tracker.service';
import { ApplicationConfigService } from 'app/core/config/application-config.service';

import { AccountService } from './account.service';

function accountWithAuthorities(authorities: string[]): Account {
  return {
    id: 0,
    activated: true,
    authorities,
    email: '',
    firstName: '',
    langKey: '',
    lastName: '',
    login: '',
    imageUrl: '',
  };
}

describe('Service Tests', () => {
  describe('Account Service', () => {
    let service: AccountService;
    let applicationConfigService: ApplicationConfigService;
    let httpMock: HttpTestingController;
    let mockStorageService: StateStorageService;
    let mockRouter: Router;
    let mockTrackerService: TrackerService;
    let mockTranslateService: TranslateService;
    let sessionStorageService: SessionStorageService;

    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, NgxWebstorageModule.forRoot()],
        providers: [TranslateService, TrackerService, StateStorageService, Router],
      });

      service = TestBed.inject(AccountService);
      applicationConfigService = TestBed.inject(ApplicationConfigService);
      httpMock = TestBed.inject(HttpTestingController);
      mockStorageService = TestBed.inject(StateStorageService);
      mockRouter = TestBed.inject(Router);
      mockTrackerService = TestBed.inject(TrackerService);
      mockTranslateService = TestBed.inject(TranslateService);
      sessionStorageService = TestBed.inject(SessionStorageService);
    });

    afterEach(() => {
      httpMock.verify();
    });

    describe('save', () => {
      it('should call account saving endpoint with correct values', () => {
        // GIVEN
        const account = accountWithAuthorities([]);

        // WHEN
        service.save(account).subscribe();
        const testRequest = httpMock.expectOne({ method: 'POST', url: applicationConfigService.getEndpointFor('api/account') });
        testRequest.flush({});

        // THEN
        expect(testRequest.request.body).toEqual(account);
      });
    });

    describe('authenticate', () => {
      it('authenticationState should emit null if input is null', () => {
        // GIVEN
        let userIdentity: Account | null = accountWithAuthorities([]);
        service.getAuthenticationState().subscribe(account => (userIdentity = account));

        // WHEN
        service.authenticate(null);

        // THEN
        expect(userIdentity).toBeNull();
        expect(service.isAuthenticated()).toBe(false);
        expect(mockTrackerService.disconnect).toHaveBeenCalled();
        expect(mockTrackerService.connect).not.toHaveBeenCalled();
      });

      it('authenticationState should emit the same account as was in input parameter', () => {
        // GIVEN
        const expectedResult = accountWithAuthorities([]);
        let userIdentity: Account | null = null;
        service.getAuthenticationState().subscribe(account => (userIdentity = account));

        // WHEN
        service.authenticate(expectedResult);

        // THEN
        expect(userIdentity).toEqual(expectedResult);
        expect(service.isAuthenticated()).toBe(true);
        expect(mockTrackerService.connect).toHaveBeenCalled();
        expect(mockTrackerService.disconnect).not.toHaveBeenCalled();
      });
    });

    describe('identity', () => {
      it('should call /account only once if not logged out after first authentication and should call /account again if user has logged out', () => {
        // Given the user is authenticated
        service.identity().subscribe();
        httpMock.expectOne({ method: 'GET' }).flush({});

        // When I call
        service.identity().subscribe();

        // Then there is no second request
        httpMock.expectNone({ method: 'GET' });

        // When I log out
        service.authenticate(null);
        // and then call
        service.identity().subscribe();

        // Then there is a new request
        httpMock.expectOne({ method: 'GET' });
      });

      describe('should change the language on authentication if necessary', () => {
        it('should change language if user has not changed language manually', () => {
          // GIVEN
          sessionStorageService.retrieve = jest.fn(key => (key === 'locale' ? undefined : 'otherSessionStorageValue'));

          // WHEN
          service.identity().subscribe();
          httpMock.expectOne({ method: 'GET' }).flush({ ...accountWithAuthorities([]), langKey: 'accountLang' });

          // THEN
          expect(mockTranslateService.use).toHaveBeenCalledWith('accountLang');
        });

        it('should not change language if user has changed language manually', () => {
          // GIVEN
          sessionStorageService.retrieve = jest.fn(key => (key === 'locale' ? 'sessionLang' : undefined));

          // WHEN
          service.identity().subscribe();
          httpMock.expectOne({ method: 'GET' }).flush({ ...accountWithAuthorities([]), langKey: 'accountLang' });

          // THEN
          expect(mockTranslateService.use).not.toHaveBeenCalled();
        });
      });

      describe('navigateToStoredUrl', () => {
        it('should navigate to the previous stored url post successful authentication', () => {
          // GIVEN
          mockStorageService.getUrl = jest.fn(() => 'admin/users?page=0');

          // WHEN
          service.identity().subscribe();
          httpMock.expectOne({ method: 'GET' }).flush({});

          // THEN
          expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1);
          expect(mockStorageService.clearUrl).toHaveBeenCalledTimes(1);
          expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('admin/users?page=0');
        });

        it('should not navigate to the previous stored url when authentication fails', () => {
          // WHEN
          service.identity().subscribe();
          httpMock.expectOne({ method: 'GET' }).error(new ErrorEvent(''));

          // THEN
          expect(mockStorageService.getUrl).not.toHaveBeenCalled();
          expect(mockStorageService.clearUrl).not.toHaveBeenCalled();
          expect(mockRouter.navigateByUrl).not.toHaveBeenCalled();
        });

        it('should not navigate to the previous stored url when no such url exists post successful authentication', () => {
          // GIVEN
          mockStorageService.getUrl = jest.fn(() => null);

          // WHEN
          service.identity().subscribe();
          httpMock.expectOne({ method: 'GET' }).flush({});

          // THEN
          expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1);
          expect(mockStorageService.clearUrl).not.toHaveBeenCalled();
          expect(mockRouter.navigateByUrl).not.toHaveBeenCalled();
        });
      });
    });

    describe('hasAnyAuthority', () => {
      describe('hasAnyAuthority string parameter', () => {
        it('should return false if user is not logged', () => {
          const hasAuthority = service.hasAnyAuthority(Authority.USER);
          expect(hasAuthority).toBe(false);
        });

        it('should return false if user is logged and has not authority', () => {
          service.authenticate(accountWithAuthorities([Authority.USER]));

          const hasAuthority = service.hasAnyAuthority(Authority.ADMIN);

          expect(hasAuthority).toBe(false);
        });

        it('should return true if user is logged and has authority', () => {
          service.authenticate(accountWithAuthorities([Authority.USER]));

          const hasAuthority = service.hasAnyAuthority(Authority.USER);

          expect(hasAuthority).toBe(true);
        });
      });

      describe('hasAnyAuthority array parameter', () => {
        it('should return false if user is not logged', () => {
          const hasAuthority = service.hasAnyAuthority([Authority.USER]);
          expect(hasAuthority).toBeFalsy();
        });

        it('should return false if user is logged and has not authority', () => {
          service.authenticate(accountWithAuthorities([Authority.USER]));

          const hasAuthority = service.hasAnyAuthority([Authority.ADMIN]);

          expect(hasAuthority).toBe(false);
        });

        it('should return true if user is logged and has authority', () => {
          service.authenticate(accountWithAuthorities([Authority.USER]));

          const hasAuthority = service.hasAnyAuthority([Authority.USER, Authority.ADMIN]);

          expect(hasAuthority).toBe(true);
        });
      });
    });
  });
});
