import { TestBed, spyOnProperty } from '@/testing';
import {
  EmptyError,
  ReplaySubject,
  firstValueFrom,
  of,
  throwError,
} from 'rxjs';
import { delay, share } from 'rxjs/operators';
import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service';
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
import { SilentRenewService } from '../iframe/silent-renew.service';
import { LoggerService } from '../logging/logger.service';
import type { LoginResponse } from '../login/login-response';
import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { UserService } from '../user-data/user.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
import {
  MAX_RETRY_ATTEMPTS,
  RefreshSessionService,
} from './refresh-session.service';

describe('RefreshSessionService ', () => {
  vi.useFakeTimers();
  let refreshSessionService: RefreshSessionService;
  let flowHelper: FlowHelper;
  let authStateService: AuthStateService;
  let silentRenewService: SilentRenewService;
  let storagePersistenceService: StoragePersistenceService;
  let flowsDataService: FlowsDataService;
  let refreshSessionIframeService: RefreshSessionIframeService;
  let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService;
  let authWellKnownService: AuthWellKnownService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [],
      providers: [
        FlowHelper,
        mockProvider(FlowsDataService),
        RefreshSessionService,
        mockProvider(LoggerService),
        mockProvider(SilentRenewService),
        mockProvider(AuthStateService),
        mockProvider(AuthWellKnownService),
        mockProvider(RefreshSessionIframeService),
        mockProvider(StoragePersistenceService),
        mockProvider(RefreshSessionRefreshTokenService),
        mockProvider(UserService),
        mockProvider(PublicEventsService),
      ],
    });
    refreshSessionService = TestBed.inject(RefreshSessionService);
    flowsDataService = TestBed.inject(FlowsDataService);
    flowHelper = TestBed.inject(FlowHelper);
    authStateService = TestBed.inject(AuthStateService);
    refreshSessionIframeService = TestBed.inject(RefreshSessionIframeService);
    refreshSessionRefreshTokenService = TestBed.inject(
      RefreshSessionRefreshTokenService
    );
    silentRenewService = TestBed.inject(SilentRenewService);
    authWellKnownService = TestBed.inject(AuthWellKnownService);
    storagePersistenceService = TestBed.inject(StoragePersistenceService);
  });

  // biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
  afterEach(() => {
    vi.useRealTimers();
  });

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

  describe('userForceRefreshSession', () => {
    it('should persist params refresh when extra custom params given and useRefreshToken is true', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        true
      );
      const writeSpy = vi.spyOn(storagePersistenceService, 'write');
      const allConfigs = [
        {
          configId: 'configId1',
          useRefreshToken: true,
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const extraCustomParams = { extra: 'custom' };

      await firstValueFrom(
        refreshSessionService.userForceRefreshSession(
          allConfigs[0]!,
          allConfigs,
          extraCustomParams
        )
      );
      expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
        'storageCustomParamsRefresh',
        extraCustomParams,
        allConfigs[0]
      );
    });

    it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        true
      );
      const allConfigs = [
        {
          configId: 'configId1',
          useRefreshToken: false,
          silentRenewTimeoutInSeconds: 10,
        },
      ];
      const writeSpy = vi.spyOn(storagePersistenceService, 'write');

      const extraCustomParams = { extra: 'custom' };

      await firstValueFrom(
        refreshSessionService.userForceRefreshSession(
          allConfigs[0]!,
          allConfigs,
          extraCustomParams
        )
      );
      expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
        'storageCustomParamsAuthRequest',
        extraCustomParams,
        allConfigs[0]
      );
    });

    it('should NOT persist customparams if no customparams are given', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        true
      );
      const allConfigs = [
        {
          configId: 'configId1',
          useRefreshToken: false,
          silentRenewTimeoutInSeconds: 10,
        },
      ];
      const writeSpy = vi.spyOn(storagePersistenceService, 'write');

      await firstValueFrom(
        refreshSessionService.userForceRefreshSession(
          allConfigs[0]!,
          allConfigs
        )
      );
      expect(writeSpy).not.toHaveBeenCalled();
    });

    it('should call resetSilentRenewRunning in case of an error', async () => {
      vi.spyOn(refreshSessionService, 'forceRefreshSession').mockReturnValue(
        throwError(() => new Error('error'))
      );
      vi.spyOn(flowsDataService, 'resetSilentRenewRunning');
      const allConfigs = [
        {
          configId: 'configId1',
          useRefreshToken: false,
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      try {
        const result = await firstValueFrom(
          refreshSessionService.userForceRefreshSession(
            allConfigs[0]!,
            allConfigs
          )
        );
        if (result) {
          expect.fail('It should not return any result.');
        } else {
          expect(
            flowsDataService.resetSilentRenewRunning
          ).toHaveBeenCalledExactlyOnceWith(allConfigs[0]);
        }
      } catch (error: any) {
        expect(error).toBeInstanceOf(Error);
      }
    });
    it('should call resetSilentRenewRunning in case of no error', async () => {
      vi.spyOn(refreshSessionService, 'forceRefreshSession').mockReturnValue(
        of({} as LoginResponse)
      );
      vi.spyOn(flowsDataService, 'resetSilentRenewRunning');
      const allConfigs = [
        {
          configId: 'configId1',
          useRefreshToken: false,
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      try {
        await firstValueFrom(
          refreshSessionService.userForceRefreshSession(
            allConfigs[0]!,
            allConfigs
          )
        );
      } catch (err: any) {
        if (err instanceof EmptyError) {
          expect(
            flowsDataService.resetSilentRenewRunning
          ).toHaveBeenCalledExactlyOnceWith(allConfigs[0]);
        } else {
          expect.fail('It should not return any error.');
        }
      }
    });
  });

  describe('forceRefreshSession', () => {
    it('only calls start refresh session and returns idToken and accessToken if auth is true', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        true
      );
      vi.spyOn(authStateService, 'getIdToken').mockReturnValue('id-token');
      vi.spyOn(authStateService, 'getAccessToken').mockReturnValue(
        'access-token'
      );
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const result = await firstValueFrom(
        refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
      );
      expect(result.idToken).toEqual('id-token');
      expect(result.accessToken).toEqual('access-token');
    });

    it('only calls start refresh session and returns null if auth is false', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        false
      );
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const result = await firstValueFrom(
        refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
      );
      expect(result).toEqual({
        isAuthenticated: false,
        errorMessage: '',
        userData: null,
        idToken: '',
        accessToken: '',
        configId: 'configId1',
      });
    });

    it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(false);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        true
      );
      spyOnProperty(
        silentRenewService,
        'refreshSessionWithIFrameCompleted$'
      ).mockReturnValue(
        of({
          authResult: {
            id_token: 'some-id_token',
            access_token: 'some-access_token',
          },
        } as CallbackContext)
      );
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const result = await firstValueFrom(
        refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
      );
      expect(result.idToken).toBeDefined();
      expect(result.accessToken).toBeDefined();
    });

    it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', async () => {
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(false);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        false
      );
      spyOnProperty(
        silentRenewService,
        'refreshSessionWithIFrameCompleted$'
      ).mockReturnValue(of(null));
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const result = await firstValueFrom(
        refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
      );
      expect(result).toEqual({
        isAuthenticated: false,
        errorMessage: '',
        userData: null,
        idToken: '',
        accessToken: '',
        configId: 'configId1',
      });
    });

    it('occurs timeout error and retry mechanism exhausted max retry count throws error', async () => {
      vi.useRealTimers();
      vi.useFakeTimers();
      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(false);
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(of(null));
      spyOnProperty(
        silentRenewService,
        'refreshSessionWithIFrameCompleted$'
      ).mockReturnValue(of(null).pipe(delay(11000)));

      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        false
      );
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const resetSilentRenewRunningSpy = vi.spyOn(
        flowsDataService,
        'resetSilentRenewRunning'
      );
      const expectedInvokeCount = MAX_RETRY_ATTEMPTS;

      try {
        const o$ = refreshSessionService
          .forceRefreshSession(allConfigs[0]!, allConfigs)
          .pipe(
            share({
              connector: () => new ReplaySubject(1),
              resetOnError: false,
              resetOnComplete: false,
              resetOnRefCountZero: true,
            })
          );

        o$.subscribe();

        await vi.advanceTimersByTimeAsync(
          allConfigs[0]!.silentRenewTimeoutInSeconds * 10000
        );

        const result = await firstValueFrom(o$);

        if (result) {
          expect.fail('It should not return any result.');
        }
      } catch (error: any) {
        expect(error).toBeInstanceOf(Error);
        expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(
          expectedInvokeCount
        );
      }
    });

    it('occurs unknown error throws it to subscriber', async () => {
      const allConfigs = [
        {
          configId: 'configId1',
          silentRenewTimeoutInSeconds: 10,
        },
      ];

      const expectedErrorMessage = 'Test error message';

      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(false);
      spyOnProperty(
        silentRenewService,
        'refreshSessionWithIFrameCompleted$'
      ).mockReturnValue(of(null));
      vi.spyOn(
        refreshSessionService as any,
        'startRefreshSession'
      ).mockReturnValue(throwError(() => new Error(expectedErrorMessage)));
      vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
        false
      );

      const resetSilentRenewRunningSpy = vi.spyOn(
        flowsDataService,
        'resetSilentRenewRunning'
      );

      try {
        await firstValueFrom(
          refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
        );
        expect.fail('It should not return any result.');
      } catch (error: any) {
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toEqual(`Error: ${expectedErrorMessage}`);
        expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled();
      }
    });

    describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => {
      it('does return null when not authenticated', async () => {
        const allConfigs = [
          {
            configId: 'configId1',
            silentRenewTimeoutInSeconds: 10,
          },
        ];

        vi.spyOn(
          flowHelper,
          'isCurrentFlowCodeFlowWithRefreshTokens'
        ).mockReturnValue(false);
        vi.spyOn(
          refreshSessionService as any,
          'startRefreshSession'
        ).mockReturnValue(of(null));
        vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
          false
        );
        spyOnProperty(
          silentRenewService,
          'refreshSessionWithIFrameCompleted$'
        ).mockReturnValue(of(null));

        const result = await firstValueFrom(
          refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
        );
        expect(result).toEqual({
          isAuthenticated: false,
          errorMessage: '',
          userData: null,
          idToken: '',
          accessToken: '',
          configId: 'configId1',
        });
      });

      it('return value only returns once', async () => {
        const allConfigs = [
          {
            configId: 'configId1',
            silentRenewTimeoutInSeconds: 10,
          },
        ];

        vi.spyOn(
          flowHelper,
          'isCurrentFlowCodeFlowWithRefreshTokens'
        ).mockReturnValue(false);
        vi.spyOn(
          refreshSessionService as any,
          'startRefreshSession'
        ).mockReturnValue(of(null));
        spyOnProperty(
          silentRenewService,
          'refreshSessionWithIFrameCompleted$'
        ).mockReturnValue(
          of({
            authResult: {
              id_token: 'some-id_token',
              access_token: 'some-access_token',
            },
          } as CallbackContext)
        );
        const spyInsideMap = vi
          .spyOn(authStateService, 'areAuthStorageTokensValid')
          .mockReturnValue(true);

        const result = await firstValueFrom(
          refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
        );
        expect(result).toEqual({
          idToken: 'some-id_token',
          accessToken: 'some-access_token',
          isAuthenticated: true,
          userData: undefined,
          configId: 'configId1',
        });
        expect(spyInsideMap).toHaveBeenCalledTimes(1);
      });
    });
  });

  describe('startRefreshSession', () => {
    it('returns null if no auth well known endpoint defined', async () => {
      vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);

      const result = await firstValueFrom(
        (refreshSessionService as any).startRefreshSession()
      );
      expect(result).toBe(null);
    });

    it('returns null if silent renew Is running', async () => {
      vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);

      const result = await firstValueFrom(
        (refreshSessionService as any).startRefreshSession()
      );
      expect(result).toBe(null);
    });

    it('calls `setSilentRenewRunning` when should be executed', async () => {
      const setSilentRenewRunningSpy = vi.spyOn(
        flowsDataService,
        'setSilentRenewRunning'
      );
      const allConfigs = [
        {
          configId: 'configId1',
          authWellknownEndpointUrl: 'https://authWell',
        },
      ];

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

      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      vi.spyOn(
        refreshSessionRefreshTokenService,
        'refreshSessionWithRefreshTokens'
      ).mockReturnValue(of({} as CallbackContext));

      await firstValueFrom(
        (refreshSessionService as any).startRefreshSession(
          allConfigs[0]!,
          allConfigs
        )
      );
      expect(setSilentRenewRunningSpy).toHaveBeenCalled();
    });

    it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', async () => {
      vi.spyOn(flowsDataService, 'setSilentRenewRunning');
      const allConfigs = [
        {
          configId: 'configId1',
          authWellknownEndpointUrl: 'https://authWell',
        },
      ];

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

      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(true);
      const refreshSessionWithRefreshTokensSpy = vi
        .spyOn(
          refreshSessionRefreshTokenService,
          'refreshSessionWithRefreshTokens'
        )
        .mockReturnValue(of({} as CallbackContext));

      await firstValueFrom(
        (refreshSessionService as any).startRefreshSession(
          allConfigs[0]!,
          allConfigs
        )
      );
      expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
    });

    it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', async () => {
      vi.spyOn(flowsDataService, 'setSilentRenewRunning');
      const allConfigs = [
        {
          configId: 'configId1',
          authWellknownEndpointUrl: 'https://authWell',
        },
      ];

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

      vi.spyOn(
        flowHelper,
        'isCurrentFlowCodeFlowWithRefreshTokens'
      ).mockReturnValue(false);
      const refreshSessionWithRefreshTokensSpy = vi
        .spyOn(
          refreshSessionRefreshTokenService,
          'refreshSessionWithRefreshTokens'
        )
        .mockReturnValue(of({} as CallbackContext));

      const refreshSessionWithIframeSpy = vi
        .spyOn(refreshSessionIframeService, 'refreshSessionWithIframe')
        .mockReturnValue(of(false));

      await firstValueFrom(
        (refreshSessionService as any).startRefreshSession(
          allConfigs[0]!,
          allConfigs
        )
      );
      expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled();
      expect(refreshSessionWithIframeSpy).toHaveBeenCalled();
    });
  });
});
