import { TestBed, spyOnProperty } from '@/testing';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import { CheckAuthService } from '../../auth-state/check-auth.service';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock';
import { RedirectService } from '../../utils/redirect/redirect.service';
import { UrlService } from '../../utils/url/url.service';
import type { LoginResponse } from '../login-response';
import type { PopupResult } from '../popup/popup-result';
import { PopUpService } from '../popup/popup.service';
import { ResponseTypeValidationService } from '../response-type-validation/response-type-validation.service';
import { ParLoginService } from './par-login.service';
import type { ParResponse } from './par-response';
import { ParService } from './par.service';

describe('ParLoginService', () => {
  let service: ParLoginService;
  let responseTypeValidationService: ResponseTypeValidationService;
  let loggerService: LoggerService;
  let authWellKnownService: AuthWellKnownService;
  let parService: ParService;
  let urlService: UrlService;
  let redirectService: RedirectService;
  let popupService: PopUpService;
  let checkAuthService: CheckAuthService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ParLoginService,
        mockProvider(LoggerService),
        mockProvider(ResponseTypeValidationService),
        mockProvider(UrlService),
        mockProvider(RedirectService),
        mockProvider(AuthWellKnownService),
        mockProvider(PopUpService),
        mockProvider(CheckAuthService),
        mockProvider(ParService),
      ],
    });
    service = TestBed.inject(ParLoginService);
    loggerService = TestBed.inject(LoggerService);
    responseTypeValidationService = TestBed.inject(
      ResponseTypeValidationService
    );
    authWellKnownService = TestBed.inject(AuthWellKnownService);
    parService = TestBed.inject(ParService);
    urlService = TestBed.inject(UrlService);
    redirectService = TestBed.inject(RedirectService);
    popupService = TestBed.inject(PopUpService);
    checkAuthService = TestBed.inject(CheckAuthService);
  });

  it('should create', () => {
    expect(service).toBeTruthy();
  });

  describe('loginPar', () => {
    it('does nothing if it has an invalid response type', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(false);
      const loggerSpy = vi.spyOn(loggerService, 'logError');

      const result = await firstValueFrom(service.loginPar({}));

      expect(result).toBeUndefined();
      expect(loggerSpy).toHaveBeenCalled();
    });

    it('calls parService.postParRequest without custom params when no custom params are passed', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      const spy = vi
        .spyOn(parService, 'postParRequest')
        .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));

      const result = await firstValueFrom(
        service.loginPar({
          authWellknownEndpointUrl: 'authWellknownEndpoint',
          responseType: 'stubValue',
        })
      );

      expect(result).toBeUndefined();
      expect(spy).toHaveBeenCalled();
    });

    it('calls parService.postParRequest with custom params when custom params are passed', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      const spy = vi
        .spyOn(parService, 'postParRequest')
        .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));

      const result = await firstValueFrom(
        service.loginPar(config, {
          customParams: { some: 'thing' },
        })
      );

      expect(result).toBeUndefined();
      expect(spy).toHaveBeenCalledExactlyOnceWith(config, {
        customParams: { some: 'thing' },
      });
    });

    it('returns undefined and logs error when no url could be created', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue('');
      const spy = vi.spyOn(loggerService, 'logError');

      const result = await firstValueFrom(service.loginPar(config));

      expect(result).toBeUndefined();
      expect(spy).toHaveBeenCalledTimes(1);
    });

    it('calls redirect service redirectTo when url could be created', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };

      const authOptions = {};

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(
        'some-par-url'
      );
      const spy = vi.spyOn(redirectService, 'redirectTo');

      await firstValueFrom(service.loginPar(config, authOptions));

      expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url');
    });

    it('calls urlHandler when URL is passed', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(
        'some-par-url'
      );
      const redirectToSpy = vi.spyOn(redirectService, 'redirectTo');
      const spy = vi.fn();
      const urlHandler = (url: any): void => {
        spy(url);
      };

      await firstValueFrom(service.loginPar(config, { urlHandler }));

      expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url');
      expect(redirectToSpy).not.toHaveBeenCalled();
    });
  });

  describe('loginWithPopUpPar', () => {
    it('does nothing if it has an invalid response type', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(false);
      const loggerSpy = vi.spyOn(loggerService, 'logError');
      const config = {};
      const allConfigs = [config];

      try {
        await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
      } catch (err: any) {
        expect(loggerSpy).toHaveBeenCalled();
        expect(err.message).toBe('Invalid response type!');
      }
    });

    it('calls parService.postParRequest without custom params when no custom params are passed', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      const spy = vi
        .spyOn(parService, 'postParRequest')
        .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));

      try {
        await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
      } catch (err: any) {
        expect(spy).toHaveBeenCalled();
        expect(err.message).toBe(
          "Could not create URL with param requestUri: 'url'"
        );
      }
    });

    it('calls parService.postParRequest with custom params when custom params are passed', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      const spy = vi
        .spyOn(parService, 'postParRequest')
        .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));

      try {
        await firstValueFrom(
          service.loginWithPopUpPar(config, allConfigs, {
            customParams: { some: 'thing' },
          })
        );
      } catch (err: any) {
        expect(spy).toHaveBeenCalledExactlyOnceWith(config, {
          customParams: { some: 'thing' },
        });
        expect(err.message).toBe(
          "Could not create URL with param requestUri: 'url'"
        );
      }
    });

    it('returns undefined and logs error when no URL could be created', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue('');
      const spy = vi.spyOn(loggerService, 'logError');

      try {
        await firstValueFrom(
          service.loginWithPopUpPar(config, allConfigs, {
            customParams: { some: 'thing' },
          })
        );
      } catch (err: any) {
        expect(err.message).toBe(
          "Could not create URL with param requestUri: 'url'"
        );
        expect(spy).toHaveBeenCalledTimes(1);
      }
    });

    it('calls popupService openPopUp when URL could be created', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(
        'some-par-url'
      );
      vi.spyOn(checkAuthService, 'checkAuth').mockReturnValue(
        of({} as LoginResponse)
      );
      spyOnProperty(popupService, 'result$').mockReturnValue(
        of({} as PopupResult)
      );
      const spy = vi.spyOn(popupService, 'openPopUp');

      await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
      expect(spy).toHaveBeenCalledExactlyOnceWith(
        'some-par-url',
        undefined,
        config
      );
    });

    it('returns correct properties if URL is received', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
        configId: 'configId1',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(
        'some-par-url'
      );

      const checkAuthSpy = vi
        .spyOn(checkAuthService, 'checkAuth')
        .mockReturnValue(
          of({
            isAuthenticated: true,
            configId: 'configId1',
            idToken: '',
            userData: { any: 'userData' },
            accessToken: 'anyAccessToken',
          })
        );
      const popupResult: PopupResult = {
        userClosed: false,
        receivedUrl: 'someUrl',
      };

      spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));

      const result = await firstValueFrom(
        service.loginWithPopUpPar(config, allConfigs)
      );
      expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith(
        config,
        allConfigs,
        'someUrl'
      );
      expect(result).toEqual({
        isAuthenticated: true,
        configId: 'configId1',
        idToken: '',
        userData: { any: 'userData' },
        accessToken: 'anyAccessToken',
      });
    });

    it('returns correct properties if popup was closed by user', async () => {
      vi.spyOn(
        responseTypeValidationService,
        'hasConfigValidResponseType'
      ).mockReturnValue(true);
      const config = {
        authWellknownEndpointUrl: 'authWellknownEndpoint',
        responseType: 'stubValue',
        configId: 'configId1',
      };
      const allConfigs = [config];

      vi.spyOn(
        authWellKnownService,
        'queryAndStoreAuthWellKnownEndPoints'
      ).mockReturnValue(of({}));

      vi.spyOn(parService, 'postParRequest').mockReturnValue(
        of({ requestUri: 'requestUri' } as ParResponse)
      );
      vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(
        'some-par-url'
      );

      const checkAuthSpy = vi.spyOn(checkAuthService, 'checkAuth');
      const popupResult = { userClosed: true } as PopupResult;

      spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));

      const result = await firstValueFrom(
        service.loginWithPopUpPar(config, allConfigs)
      );
      expect(checkAuthSpy).not.toHaveBeenCalled();
      expect(result).toEqual({
        isAuthenticated: false,
        errorMessage: 'User closed popup',
        configId: 'configId1',
        idToken: '',
        userData: null,
        accessToken: '',
      });
    });
  });
});
