import { Mock, mock } from '@alfa-client/test-utils';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { faker } from '@faker-js/faker';
import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client';
import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client';
import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';
import { Groups } from '@keycloak/keycloak-admin-client/lib/resources/groups';
import { OAuthService } from 'angular-oauth2-oidc';
import { Observable, OperatorFunction, catchError, firstValueFrom, of, throwError } from 'rxjs';
import {
  createGroupRepresentation,
  createNetworkError,
  createOrganisationseinheit,
  createOrganisationseinheitError,
} from '../../../test/user/user';
import {
  Organisationseinheit,
  OrganisationseinheitError,
  OrganisationseinheitErrorType,
} from './user.model';
import { UserRepository } from './user.repository.service';

describe('UserRepository', () => {
  const accessToken: string = faker.random.alphaNumeric(40);
  const error: OrganisationseinheitError = createOrganisationseinheitError();
  const networkError: NetworkError = createNetworkError(400, '');

  let repository: UserRepository;

  let kcAdminClient: Mock<KcAdminClient>;
  let oAuthService: Mock<OAuthService>;

  const mockGroupsFunc = (groupsKey: keyof Groups, implementation: jest.Mock) => {
    kcAdminClient.groups = <any>{
      [groupsKey]: implementation,
    };
  };

  beforeEach(() => {
    kcAdminClient = mock(KcAdminClient);
    oAuthService = mock(OAuthService);
    TestBed.configureTestingModule({
      providers: [
        { provide: OAuthService, useValue: oAuthService },
        { provide: KcAdminClient, useValue: kcAdminClient },
      ],
    });
    oAuthService.getAccessToken.mockReturnValue(accessToken);
    repository = TestBed.inject(UserRepository);
  });

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

  describe('registerTokenProvider', () => {
    it('should register token provider from oauth service', async () => {
      const tokenProvider: TokenProvider = kcAdminClient.registerTokenProvider.mock.calls[0][0];
      const token: string = await tokenProvider.getAccessToken();
      expect(token).toEqual(accessToken);
    });
  });

  describe('map organisationseinheit representation', () => {
    let expectedOrganisationseinheit: Organisationseinheit = createOrganisationseinheit();

    it('should map field "id"', () => {
      const organisationseinheit: Organisationseinheit =
        repository.mapGroupRepresentationToOrganisationseinheit({
          id: expectedOrganisationseinheit.id,
        });

      expect(organisationseinheit.id).toEqual(expectedOrganisationseinheit.id);
    });

    it('should map field "name"', () => {
      const organisationseinheit: Organisationseinheit =
        repository.mapGroupRepresentationToOrganisationseinheit({
          name: expectedOrganisationseinheit.name,
        });

      expect(organisationseinheit.name).toEqual(expectedOrganisationseinheit.name);
    });

    it('should map field "organisationseinheitIds"', () => {
      const organisationseinheit: Organisationseinheit =
        repository.mapGroupRepresentationToOrganisationseinheit({
          attributes: {
            organisationseinheitId: expectedOrganisationseinheit.organisationseinheitIds,
          },
        });

      expect(organisationseinheit.organisationseinheitIds).toEqual(
        expectedOrganisationseinheit.organisationseinheitIds,
      );
    });

    it('should map missing organisationseinheitIds to empty list', () => {
      const organisationseinheit: Organisationseinheit =
        repository.mapGroupRepresentationToOrganisationseinheit({});

      expect(organisationseinheit.organisationseinheitIds).toEqual([]);
    });
  });

  describe('find organisationseinheitItems', () => {
    const organisationseinheitItems: Organisationseinheit[] = [
      createOrganisationseinheit(),
      createOrganisationseinheit(),
      createOrganisationseinheit(),
    ];

    const groupReps: GroupRepresentation[] =
      organisationseinheitItems.map(createGroupRepresentation);

    it('should return mapped organisationseinheit search result', async () => {
      const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps));
      mockGroupsFunc('find', findMock);

      const groupsResult: Organisationseinheit[] = await firstValueFrom(
        repository.findOrganisationseinheitItems(),
      );

      expect(groupsResult).toEqual(groupsResult);
    });

    it('should call with brief representation', fakeAsync(() => {
      const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps));
      mockGroupsFunc('find', findMock);

      repository.findOrganisationseinheitItems().subscribe();
      tick();

      expect(findMock).toHaveBeenCalledWith({ briefRepresentation: false });
    }));
  });

  describe('save organisationseinheit', () => {
    const saveGroup: Organisationseinheit = createOrganisationseinheit();

    it('should call kcAdminClient.groups.save', async () => {
      const updateMock: jest.Mock = jest.fn(() => of(null));
      mockGroupsFunc('update', updateMock);

      await firstValueFrom(repository.saveOrganisationseinheit(saveGroup));

      expect(updateMock).toHaveBeenCalledWith(
        { id: saveGroup.id },
        {
          name: saveGroup.name,
          attributes: {
            organisationseinheitId: saveGroup.organisationseinheitIds,
          },
        },
      );
    });

    it('should return organisationseinheit save observable', async () => {
      const updateMock: jest.Mock = jest.fn(() => Promise.resolve(null));
      mockGroupsFunc('update', updateMock);

      const voidResult = await firstValueFrom(repository.saveOrganisationseinheit(saveGroup));

      expect(voidResult).toBe(null);
    });

    it('should pipe rethrowMappedGroupsError', (done) => {
      const updateMock: jest.Mock = jest.fn(() => Promise.reject(networkError));
      mockGroupsFunc('update', updateMock);
      repository.rethrowMappedGroupsError = jest
        .fn()
        .mockReturnValue(catchError(() => throwError(() => error)));

      repository.saveOrganisationseinheit(saveGroup).subscribe({
        error: (err) => {
          expect(err).toBe(error);
          done();
        },
      });
    });
  });

  describe('create organisationseinheit', () => {
    const newOrganisationseinheit: Organisationseinheit = createOrganisationseinheit();

    it('should call kcAdminClient.groups.create', async () => {
      const createMock: jest.Mock = jest.fn(() => of({ id: newOrganisationseinheit.id }));
      mockGroupsFunc('create', createMock);

      await firstValueFrom(
        repository.createOrganisationseinheit(
          newOrganisationseinheit.name,
          newOrganisationseinheit.organisationseinheitIds,
        ),
      );

      expect(createMock).toHaveBeenCalledWith({
        name: newOrganisationseinheit.name,
        attributes: {
          organisationseinheitId: newOrganisationseinheit.organisationseinheitIds,
        },
      });
    });

    it('should return mapped organisationseinheit result', async () => {
      const createMock: jest.Mock = jest.fn(() =>
        Promise.resolve({ id: newOrganisationseinheit.id }),
      );
      mockGroupsFunc('create', createMock);

      const newGroupResult: Organisationseinheit = await firstValueFrom(
        repository.createOrganisationseinheit(
          newOrganisationseinheit.name,
          newOrganisationseinheit.organisationseinheitIds,
        ),
      );

      expect(newGroupResult).toEqual(newOrganisationseinheit);
    });

    it('should pipe rethrowMappedGroupsError', (done) => {
      const createMock: jest.Mock = jest.fn(() => Promise.reject(networkError));
      mockGroupsFunc('create', createMock);
      repository.rethrowMappedGroupsError = jest
        .fn()
        .mockReturnValue(catchError(() => throwError(() => error)));

      repository
        .createOrganisationseinheit(
          newOrganisationseinheit.name,
          newOrganisationseinheit.organisationseinheitIds,
        )
        .subscribe({
          error: (err) => {
            expect(err).toBe(error);
            done();
          },
        });
    });
  });

  describe('rethrow mapped groups error', () => {
    let networkErrorObservable: Observable<never>;

    beforeEach(() => {
      repository.mapCreateGroupsNetworkError = jest.fn().mockReturnValue(error);
      networkErrorObservable = throwError(() => networkError);
    });

    it('should throw mapped error', (done) => {
      const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError();

      networkErrorObservable.pipe(rethrowOperator).subscribe({
        error: (err) => {
          expect(err).toBe(error);
          done();
        },
      });
    });

    it('should call mapCreateGroupsNetworkError', (done) => {
      const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError();

      networkErrorObservable.pipe(rethrowOperator).subscribe({
        error: () => {
          expect(repository.mapCreateGroupsNetworkError).toHaveBeenCalledWith(networkError);
          done();
        },
      });
    });
  });

  describe('map create groups network error', () => {
    it('should interpret 409 status as name conflict', () => {
      const keycloakError: OrganisationseinheitError = createOrganisationseinheitError(
        OrganisationseinheitErrorType.NAME_CONFLICT,
      );
      const networkError: NetworkError = createNetworkError(409, keycloakError.detail);

      const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError);

      expect(error).toEqual(keycloakError);
    });

    it('should interpret 400 status as name missing', () => {
      const keycloakError: OrganisationseinheitError = createOrganisationseinheitError(
        OrganisationseinheitErrorType.NAME_MISSING,
      );
      const networkError: NetworkError = createNetworkError(400, keycloakError.detail);

      const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError);

      expect(error).toEqual(keycloakError);
    });

    it('should map missing errorMessage to empty string', () => {
      const networkError: NetworkError = createNetworkError(500, undefined);

      const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError);
      expect(error.detail).toEqual('');
    });
  });

  describe('delete organisationseinheit', () => {
    const deleteOrganisationseinheit: Organisationseinheit = createOrganisationseinheit();

    it('should call kcAdminClient.groups.del', async () => {
      const delMock: jest.Mock = jest.fn(() =>
        Promise.resolve({ id: deleteOrganisationseinheit.id }),
      );
      mockGroupsFunc('del', delMock);

      await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id));

      expect(delMock).toHaveBeenCalledWith({
        id: deleteOrganisationseinheit.id,
      });
    });

    it('should return void', async () => {
      mockGroupsFunc(
        'del',
        jest.fn(() => Promise.resolve(null)),
      );

      const voidResult = await firstValueFrom(
        repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id),
      );

      expect(voidResult).toBeNull();
    });
  });
});