import { Config } from '@core/config';
import { SpectatorService, createServiceFactory, SpyObject } from '@ngneat/spectator';
import { OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

describe('AuthService', () => {
  let spectator: SpectatorService<AuthService>;
  const createService = createServiceFactory({
    service: AuthService,
    mocks: [Config, OAuthService],
  });
  let config: SpyObject<Config>;
  let oAuthService: SpyObject<OAuthService>;

  beforeEach(() => {
    spectator = createService();
    config = spectator.inject(Config);
    oAuthService = spectator.inject(OAuthService);
  });

  it('should be created', () => {
    expect(spectator.service).toBeTruthy();
  });

  describe('init()', () => {
    it('should configure the oAuthService', () => {
      config.get.and.returnValue({});
      spectator.service.init();
      expect(oAuthService.configure).toHaveBeenCalledWith({
        redirectUri: window.location.origin,
        silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
        useSilentRefresh: true,
      });
      expect(oAuthService.tokenValidationHandler).toBeInstanceOf(JwksValidationHandler);
      expect(oAuthService.setupAutomaticSilentRefresh).toHaveBeenCalled();
    });

    it('should load the discovery document', async () => {
      config.get.and.returnValue({});
      oAuthService.loadDiscoveryDocumentAndTryLogin.and.returnValue(Promise.resolve(true));

      spyOn(spectator.service['_initialized'], 'next');
      await spectator.service.init();

      expect(oAuthService.loadDiscoveryDocumentAndTryLogin).toHaveBeenCalled();
      expect(spectator.service['_initialized'].next).toHaveBeenCalledWith(true);
    });

    it('should throw an error if its already initialized', async () => {
      spyOn(spectator.service['_initialized'], 'getValue').and.returnValue(true);
      await expectAsync(spectator.service.init()).toBeRejectedWithError('AuthService is already initialized');
    });
  });

  describe('isAuthenticated()', () => {
    it('should call hasValidIdToken() and return its value', () => {
      oAuthService.hasValidIdToken.and.returnValue(true);
      expect(spectator.service.isAuthenticated()).toBeTrue();
      expect(oAuthService.hasValidIdToken).toHaveBeenCalled();
    });
  });

  describe('getToken()', () => {
    it('should call getAccessToken() and return its value', () => {
      oAuthService.getAccessToken.and.returnValue('token');
      expect(spectator.service.getToken()).toEqual('token');
      expect(oAuthService.getAccessToken).toHaveBeenCalled();
    });
  });

  describe('getClaims()', () => {
    it('should call getAccessToken() and return its value', () => {
      oAuthService.getAccessToken.and.returnValue('token');
      const claims = {
        claim1: 'value',
        claim2: 'value2',
      };
      spyOn(spectator.service, 'parseJwt').and.returnValue(claims);
      expect(spectator.service.getClaims()).toEqual(claims);
      expect(spectator.service.parseJwt).toHaveBeenCalledWith('token');
      expect(oAuthService.getAccessToken).toHaveBeenCalled();
    });
  });

  describe('getClaimByKey()', () => {
    it('should call getClaims() and return its key value', () => {
      spyOn(spectator.service, 'getClaims').and.returnValue({
        claim1: 'value',
        claim2: 'value2',
      });

      expect(spectator.service.getClaimByKey('claim1')).toEqual('value');
      expect(spectator.service.getClaims).toHaveBeenCalled();
    });

    it('should return null if getClaims() returns null or undefined', () => {
      spyOn(spectator.service, 'getClaims').and.returnValue(null);

      expect(spectator.service.getClaimByKey('claim1')).toBeNull();
      expect(spectator.service.getClaims).toHaveBeenCalled();
    });
  });

  describe('parseJwt()', () => {
    it('should return null if the token is null or undefined', () => {
      expect(spectator.service.parseJwt(null)).toBeNull();
      expect(spectator.service.parseJwt(undefined)).toBeNull();
    });

    it('should return the value of the key', () => {
      const token =
        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
      expect(spectator.service.parseJwt(token)).toEqual({
        sub: '1234567890',
        name: 'John Doe',
        admin: true,
      });
    });
  });

  describe('login()', () => {
    it('should call initLoginFlow()', () => {
      spectator.service.login();
      expect(oAuthService.initLoginFlow).toHaveBeenCalled();
    });
  });

  describe('logout()', () => {
    it('should call revokeTokenAndLogout()', async () => {
      await spectator.service.logout();
      expect(oAuthService.revokeTokenAndLogout).toHaveBeenCalled();
    });
  });

  describe('getToken()', () => {
    it('should return getAccessToken()', () => {
      spectator.service.getToken();
      expect(oAuthService.getAccessToken).toHaveBeenCalled();
    });
  });

  describe('initialized', () => {
    it('should return _initialized as Observable', () => {
      spyOn(spectator.service['_initialized'], 'asObservable').and.callThrough();
      expect(spectator.service.initialized$).toBeInstanceOf(Observable);
      expect(spectator.service['_initialized'].asObservable).toHaveBeenCalled();
    });
  });
});
