import { faker } from '@faker-js/faker';
import { Channel, Languages, Priority } from '@humand/notification-packages';
import { MockProxy, mock } from 'jest-mock-extended';
import { NotificationType } from '../../../../src/api/models/firebaseTokens/notification';
import { UserDAO } from '../../../../src/api/modules/users/infrastructure/dataAccessObjects/userDAO';
import { NotificationsService } from '../../../../src/api/modules/notifications/business/services/notificationsService';
import { Modules } from '../../../../src/api/modules/notifications/business/types/modules';
import { PerformanceReviewCycle } from '../../../../src/api/modules/performanceReviews/business/models/performanceReviewCycle';
import { PerformanceReviewExternalReviewer } from '../../../../src/api/modules/performanceReviews/business/models/PerformanceReviewExternalReviewer';
import { PerformanceReviewFormReview } from '../../../../src/api/modules/performanceReviews/business/models/performanceReviewFormReview';
import { PerformanceReviewTargetUser } from '../../../../src/api/modules/performanceReviews/business/models/performanceReviewTargetUser';
import { PerformanceReviewCycleStatus } from '../../../../src/api/modules/performanceReviews/business/types/performanceReviewCycleStatus';
import {
    PerformanceReviewEvaluationTypes,
    PerformanceReviewEvaluationTypesValues,
} from '../../../../src/api/modules/performanceReviews/business/types/performanceReviewEvaluationTypes';
import { PerformanceReviewNotificationsAdapter } from '../../../../src/api/modules/performanceReviews/infrastructure/adapters/performanceReviewNotificationsAdapter';
import { User } from '../../../../src/api/modules/users/business/models/user';
import { userDAOtoUser } from '../../../../src/api/modules/users/infrastructure/mappers/usersMapper';
import { DeepLinksService } from '../../../../src/api/services/deepLinks';
import { EmailsService } from '../../../../src/api/services/emails';
import { UsersService } from '../../../../src/api/modules/users/business/services/usersService';
import { fakePositiveInteger, fakePositiveIntegerArray } from '../../../common/common';
import { UserMock } from '../../users/mocks/users.mock';
import {
    generatePerformanceReviewCycleWithValues,
    generatePerformanceReviewFormReviewWithValues,
    generateRandomExternalReviewer,
    generateRandomPerformanceReviewCycle,
    generateRandomPerformanceReviewTargetUser,
} from '../common';
import { UserStatus } from '@humand-packages/common';

let emailsService: MockProxy<EmailsService>;
let notificationsService: MockProxy<NotificationsService>;
let deepLinksService: MockProxy<DeepLinksService>;
let usersService: MockProxy<UsersService>;
let notificationAdapter: PerformanceReviewNotificationsAdapter;

describe('PerformanceReviewNotificationsAdapter', () => {
    beforeEach(() => {
        emailsService = mock<EmailsService>();
        notificationsService = mock<NotificationsService>();
        deepLinksService = mock<DeepLinksService>();
        usersService = mock<UsersService>();
        notificationAdapter = new PerformanceReviewNotificationsAdapter(
            emailsService,
            notificationsService,
            deepLinksService,
            usersService,
        );
        jest.spyOn(deepLinksService, 'getLinkToPerformanceReview').mockReturnValue(faker.internet.url());
    });

    describe('sendShareReviewNotification', () => {
        let sharedUserIds: number[];
        let reviewCycle: PerformanceReviewCycle;
        let sharedUsers: UserDAO[];

        beforeEach(() => {
            sharedUserIds = fakePositiveIntegerArray();
            reviewCycle = generateRandomPerformanceReviewCycle();
            sharedUsers = sharedUserIds.map((id) =>
                UserMock.mockDAO({ id, language: faker.helpers.arrayElement(Object.values(Languages)), email: faker.internet.email() }),
            );
        });

        it('should send share review notification', async () => {
            usersService.findByPks.mockResolvedValueOnce(sharedUsers);

            await notificationAdapter.sendShareReviewNotification(sharedUserIds, reviewCycle);

            expect(usersService.findByPks).toHaveBeenCalledExactlyOnceWith(sharedUserIds, {
                attributes: ['id', 'email', 'instanceId', 'language'],
                withFirebaseToken: false,
                status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
            });
            expect(emailsService.sendPerformanceReviewCycleShareEmail).toHaveBeenCalledExactlyOnceWith(reviewCycle, sharedUsers);
            expect(deepLinksService.getLinkToPerformanceReview).toHaveBeenCalledExactlyOnceWith(reviewCycle.id);
            expect(notificationsService.queueNotification).toHaveBeenCalledExactlyOnceWith(
                [Channel.BELL, Channel.PUSH],
                expect.objectContaining({
                    bell: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    content: expect.arrayContaining([expect.toContainKeys(['userIds', 'language', 'title', 'description'])]),
                    instanceId: reviewCycle.instanceId,
                    module: Modules.PERFORMANCE_REVIEWS,
                    push: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    type: NotificationType.SHARED_REVIEW,
                }),
                Priority.LOW,
            );
        });

        it('should not send share review notification if sharedUsers has no values', async () => {
            usersService.findByPks.mockResolvedValueOnce([]);

            await notificationAdapter.sendShareReviewNotification(sharedUserIds, reviewCycle);

            expect(emailsService.sendPerformanceReviewCycleShareEmail).not.toHaveBeenCalled();
            expect(notificationsService.queueNotification).not.toHaveBeenCalled();
            expect(deepLinksService.getLinkToPerformanceReview).not.toHaveBeenCalled();
        });
    });

    describe('sendCycleStatusChangeNotifications', () => {
        let reviewCycle: PerformanceReviewCycle;
        let reviewerIds: number[];
        let reviewers: UserDAO[];
        let reviewForms: PerformanceReviewFormReview[];
        let reviewTypesByReviewer: Map<number, Set<PerformanceReviewEvaluationTypesValues>>;

        beforeEach(() => {
            reviewerIds = fakePositiveIntegerArray();
            reviewers = reviewerIds.map((id) =>
                UserMock.mockDAO({ id, language: faker.helpers.arrayElement(Object.values(Languages)), email: faker.internet.email() }),
            );
            reviewCycle = generatePerformanceReviewCycleWithValues(fakePositiveInteger(), faker.date.recent(), {
                formTemplateTypes: [
                    faker.helpers.arrayElement(
                        Object.values(PerformanceReviewEvaluationTypes).filter(
                            (type) => type !== PerformanceReviewEvaluationTypes.PEER_EXTERNAL_REVIEW,
                        ),
                    ),
                ],
            });
            reviewForms = reviewerIds.map((reviewerId) => {
                return generatePerformanceReviewFormReviewWithValues(fakePositiveInteger(), reviewCycle.id, { reviewerId });
            });
            reviewCycle.reviewForms = reviewForms;

            reviewTypesByReviewer = new Map<number, Set<PerformanceReviewEvaluationTypesValues>>();
            reviewForms.forEach((reviewForm) => {
                const types = reviewTypesByReviewer.get(reviewForm.reviewerId!) || new Set<PerformanceReviewEvaluationTypesValues>();
                types.add(reviewForm.type);
                reviewTypesByReviewer.set(reviewForm.reviewerId!, types);
            });
        });

        it('should send cycle status change notifications on start cycle', async () => {
            reviewCycle.status = PerformanceReviewCycleStatus.IN_PROGRESS;
            usersService.findByPks.mockResolvedValueOnce(reviewers);

            await notificationAdapter.sendCycleStatusChangeNotifications(reviewCycle);

            expect(usersService.findByPks).toHaveBeenCalledExactlyOnceWith(
                reviewers.map((reviewer) => reviewer.id),
                {
                    attributes: ['id', 'email', 'instanceId', 'language'],
                    withFirebaseToken: false,
                    status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
                },
            );
            expect(emailsService.sendPerformanceReviewCycleStatusChangeEmail).toHaveBeenCalledExactlyOnceWith(
                reviewCycle,
                reviewers,
                reviewTypesByReviewer,
            );
            expect(notificationsService.queueNotification).toHaveBeenCalledExactlyOnceWith(
                [Channel.BELL, Channel.PUSH],
                expect.objectContaining({
                    bell: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    content: expect.arrayContaining([expect.toContainKeys(['userId', 'language', 'title', 'description', 'deepLink'])]),
                    instanceId: reviewCycle.instanceId,
                    module: Modules.PERFORMANCE_REVIEWS,
                    push: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    type: NotificationType.NEW_REVIEWS_TO_FILL,
                }),
                Priority.LOW,
            );
        });

        it('should send cycle status change notifications on finish cycle', async () => {
            reviewCycle.status = PerformanceReviewCycleStatus.FINISHED;
            usersService.findByPks.mockResolvedValueOnce(reviewers);

            await notificationAdapter.sendCycleStatusChangeNotifications(reviewCycle);

            expect(usersService.findByPks).toHaveBeenCalledExactlyOnceWith(
                reviewers.map((reviewer) => reviewer.id),
                {
                    attributes: ['id', 'email', 'instanceId', 'language'],
                    withFirebaseToken: false,
                    status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
                },
            );
            expect(emailsService.sendPerformanceReviewCycleStatusChangeEmail).toHaveBeenCalledExactlyOnceWith(
                reviewCycle,
                reviewers,
                reviewTypesByReviewer,
            );
            expect(notificationsService.queueNotification).toHaveBeenCalledExactlyOnceWith(
                [Channel.BELL, Channel.PUSH],
                expect.objectContaining({
                    bell: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    content: expect.arrayContaining([expect.toContainKeys(['userId', 'language', 'title', 'description', 'deepLink'])]),
                    instanceId: reviewCycle.instanceId,
                    module: Modules.PERFORMANCE_REVIEWS,
                    push: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    type: NotificationType.NEW_REVIEWS_TO_SHARE,
                }),
                Priority.LOW,
            );
        });

        it('should not send cycle status change notifications if reviewers is empty', async () => {
            usersService.findByPks.mockResolvedValueOnce([]);

            await notificationAdapter.sendCycleStatusChangeNotifications(reviewCycle);
            expect(emailsService.sendPerformanceReviewCycleStatusChangeEmail).not.toHaveBeenCalled();
            expect(notificationsService.queueNotification).not.toHaveBeenCalled();
        });
    });

    describe('sendStartCycleNotificationsForExternalReviewers', () => {
        let reviewCycle: PerformanceReviewCycle;
        let externalReviewers: PerformanceReviewExternalReviewer[];

        beforeEach(() => {
            reviewCycle = generateRandomPerformanceReviewCycle();
            externalReviewers = faker.helpers.arrayElements([generateRandomExternalReviewer()], 5);
            jest.spyOn(emailsService, 'sendPerformanceReviewExternalReviewerLink').mockResolvedValue();
        });

        it('should send start cycle notifications for external reviewers', async () => {
            await notificationAdapter.sendStartCycleNotificationsForExternalReviewers(reviewCycle, externalReviewers);

            expect(emailsService.sendPerformanceReviewExternalReviewerLink).toHaveBeenCalledExactlyOnceWith(reviewCycle, externalReviewers);
        });
    });

    describe('sendPendingReviewNotifications', () => {
        let reviewCycle: PerformanceReviewCycle;
        let reviewerIds: number[];
        let reviewers: UserDAO[];

        beforeEach(() => {
            reviewCycle = generateRandomPerformanceReviewCycle();
            reviewerIds = fakePositiveIntegerArray();
            reviewers = reviewerIds.map((id) =>
                UserMock.mockDAO({ id, language: faker.helpers.arrayElement(Object.values(Languages)), email: faker.internet.email() }),
            );
        });

        it('should send pending review notifications', async () => {
            usersService.findByPks.mockResolvedValueOnce(reviewers);

            await notificationAdapter.sendPendingReviewNotifications(reviewCycle, reviewerIds);

            expect(usersService.findByPks).toHaveBeenCalledExactlyOnceWith(reviewerIds, {
                attributes: ['id', 'email', 'instanceId', 'language'],
                withFirebaseToken: false,
                status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
            });
            expect(emailsService.sendPerformanceReviewCyclePendingReviewsEmail).toHaveBeenCalledExactlyOnceWith(reviewCycle, reviewers);
            expect(notificationsService.queueNotification).toHaveBeenCalledExactlyOnceWith(
                [Channel.BELL, Channel.PUSH],
                expect.objectContaining({
                    bell: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    content: expect.arrayContaining([expect.toContainKeys(['userIds', 'language', 'title', 'description'])]),
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    deepLink: expect.not.toBeEmpty(),
                    instanceId: reviewCycle.instanceId,
                    module: Modules.PERFORMANCE_REVIEWS,
                    push: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    type: NotificationType.PENDING_REVIEWS,
                }),
                Priority.LOW,
            );
        });

        it('should not send pending review notifications if reviewers is empty', async () => {
            usersService.findByPks.mockResolvedValueOnce([]);

            await notificationAdapter.sendPendingReviewNotifications(reviewCycle, reviewerIds);

            expect(emailsService.sendPerformanceReviewCyclePendingReviewsEmail).not.toHaveBeenCalled();
            expect(notificationsService.queueNotification).not.toHaveBeenCalled();
        });
    });

    describe('sendPerformanceReviewCycleConfirmNotification', () => {
        let reviewCycle: PerformanceReviewCycle;
        let reviewed: User;
        let targetUser: PerformanceReviewTargetUser;
        let responsibleUser: UserDAO;

        beforeEach(() => {
            reviewCycle = generateRandomPerformanceReviewCycle();
            reviewed = userDAOtoUser(UserMock.mockDAO());
            targetUser = generateRandomPerformanceReviewTargetUser(reviewCycle.id, fakePositiveInteger(), fakePositiveInteger());
            responsibleUser = UserMock.mockDAO({ id: targetUser.responsibleId!, language: faker.helpers.arrayElement(Object.values(Languages)) });
        });

        it('should send performance review cycle confirm notification', async () => {
            usersService.findByPkOrNull.mockResolvedValueOnce(responsibleUser);

            await notificationAdapter.sendPerformanceReviewCycleConfirmNotification(reviewCycle, reviewed, targetUser);

            expect(usersService.findByPkOrNull).toHaveBeenCalledExactlyOnceWith(reviewCycle.instanceId, targetUser.responsibleId, {
                withFirebaseToken: false,
            });
            expect(emailsService.sendPerformanceReviewCycleConfirmEmail).toHaveBeenCalledExactlyOnceWith(reviewCycle, reviewed, responsibleUser);
            expect(notificationsService.queueNotification).toHaveBeenCalledExactlyOnceWith(
                [Channel.BELL, Channel.PUSH],
                expect.objectContaining({
                    bell: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    deepLink: expect.not.toBeEmpty(),
                    //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    description: expect.not.toBeEmpty(),
                    instanceId: reviewCycle.instanceId,
                    language: responsibleUser.language,
                    module: Modules.PERFORMANCE_REVIEWS,
                    push: { extraData: { cycleId: reviewCycle.id.toString() }, refId: reviewCycle.id },
                    //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    title: expect.not.toBeEmpty(),
                    type: NotificationType.CONFIRMED_REVIEW,
                    userIds: [responsibleUser.id],
                }),
                Priority.LOW,
            );
        });

        it('should not send performance review cycle confirm notification if responsibleUser is null', async () => {
            usersService.findByPkOrNull.mockResolvedValueOnce(null);

            await notificationAdapter.sendPerformanceReviewCycleConfirmNotification(reviewCycle, reviewed, targetUser);

            expect(emailsService.sendPerformanceReviewCycleConfirmEmail).not.toHaveBeenCalled();
            expect(notificationsService.queueNotification).not.toHaveBeenCalled();
        });

        it('should not send performance review cycle confirm notification if responsible is null', async () => {
            targetUser.responsibleId = null;

            await notificationAdapter.sendPerformanceReviewCycleConfirmNotification(reviewCycle, reviewed, targetUser);

            expect(emailsService.sendPerformanceReviewCycleConfirmEmail).not.toHaveBeenCalled();
            expect(notificationsService.queueNotification).not.toHaveBeenCalled();
        });
    });
});
