import { faker } from '@faker-js/faker/locale/en_US';
import { IllegalStateError } from '@humand-packages/common';
import { MockProxy, mock } from 'jest-mock-extended';
import { CoreForm } from '../../../../src/api/kernels/forms/business/models/coreForm';
import { FormAnswer } from '../../../../src/api/kernels/forms/business/models/formAnswer';
import { CoreFormsService } from '../../../../src/api/kernels/forms/business/services/coreFormsService';
import { FillPerformanceReviewFormReviewInterface } from '../../../../src/api/modules/performanceReviews/business/interfaces/fillPerformanceReviewFormReviewInterface';
import { PerformanceReviewGoalQuestionWithAnswerDataFields } from '../../../../src/api/modules/performanceReviews/business/models/performanceReviewGoalQuestionWithAnswer';
import { PerformanceReviewCoreFormsAdapter } from '../../../../src/api/modules/performanceReviews/infrastructure/adapters/performanceReviewCoreFormsAdapter';
import { QUESTION_TYPES } from '../../../../src/api/types/questionTypes';
import { fakePositiveInteger } from '../../../common/common';
import { checkMockCallsCount } from '../../../common/mock/mockHelpers';
import {
    generateFormAnswerWithValues,
    generateRandomCoreFormQuestion,
    generateRandomCoreFormWithQuestions,
    generateRandomFormAnswer,
    generateRandomFormQuestionAnswers,
} from '../../../kernels/forms/common';
import { generateGoalQuestionWithAnswer, generateQuestionWeightsFromIds } from '../common';

let coreFormsService: MockProxy<CoreFormsService>;
let coreFormsAdapter: PerformanceReviewCoreFormsAdapter;

describe('PerformanceReviewCoreFormsAdapter', () => {
    beforeEach(() => {
        coreFormsService = mock<CoreFormsService>();
        coreFormsAdapter = new PerformanceReviewCoreFormsAdapter(coreFormsService, 10);
    });

    describe('getForm', () => {
        let tag: string;
        let id: number;

        beforeEach(() => {
            tag = faker.string.uuid();
            id = fakePositiveInteger();
        });

        it('should return correctly if core form is found', async () => {
            coreFormsService.getVersion.mockResolvedValueOnce(generateRandomCoreFormWithQuestions(tag, id));

            const result = await coreFormsAdapter.getForm(tag, id);
            const castedResult = result as CoreForm;

            expect(result).not.toBeNull();
            expect(castedResult).toBeInstanceOf(CoreForm);
            expect(castedResult.tag).toBe(tag);
            expect(castedResult.id).toBe(id);

            checkMockCallsCount(coreFormsService.getVersion, 1);
        });

        it('should return correctly if core form is not found', async () => {
            coreFormsService.getVersion.mockResolvedValueOnce(null);

            const result = await coreFormsAdapter.getForm(tag, id);

            expect(result).toBeNull();

            checkMockCallsCount(coreFormsService.getVersion, 1);
        });

        it('should fail if the get operation fails', async () => {
            coreFormsService.getVersion.mockRejectedValueOnce(new Error());

            await expect(coreFormsAdapter.getForm(tag, id)).rejects.toThrow(Error);
        });
    });

    describe('submitFormAnswer', () => {
        let formAnswerId: number;
        let tag: string;
        let coreFormId: number;
        let params: FillPerformanceReviewFormReviewInterface;

        beforeEach(() => {
            formAnswerId = fakePositiveInteger();
            tag = faker.string.uuid();
            coreFormId = fakePositiveInteger();
            params = {
                fillingTime: fakePositiveInteger(),
                previousFormAnswerId: fakePositiveInteger(),
                questionAnswers: [],
            };
        });

        it('should return correctly if the answer is correctly submitted', async () => {
            coreFormsService.submitFormAnswer.mockResolvedValueOnce(
                generateFormAnswerWithValues(formAnswerId, tag, coreFormId, [], params.fillingTime),
            );

            const result = await coreFormsAdapter.submitFormAnswer(tag, coreFormId, params);

            expect(result).not.toBeNull();
            expect(result).toBeInstanceOf(FormAnswer);
            expect(result.id).toBe(formAnswerId);
            expect(result.tag).toBe(tag);
            expect(result.formId).toBe(coreFormId);
            expect(result.fillingTime).toBe(params.fillingTime);

            checkMockCallsCount(coreFormsService.submitFormAnswer, 1);
        });

        it('should fail if the get operation fails', async () => {
            coreFormsService.submitFormAnswer.mockRejectedValueOnce(new Error());

            await expect(coreFormsAdapter.submitFormAnswer(tag, coreFormId, params)).rejects.toThrow(Error);
        });
    });

    describe('getFormAnswer', () => {
        let id: number;

        beforeEach(() => {
            id = fakePositiveInteger();
        });

        it('should return correctly if core form is found', async () => {
            coreFormsService.getFormAnswer.mockResolvedValueOnce(generateFormAnswerWithValues(id, faker.string.uuid(), fakePositiveInteger(), []));

            const result = await coreFormsAdapter.getFormAnswer(id);
            const castedResult = result as FormAnswer;

            expect(result).not.toBeNull();
            expect(castedResult).toBeInstanceOf(FormAnswer);
            expect(castedResult.id).toBe(id);

            checkMockCallsCount(coreFormsService.getFormAnswer, 1);
        });

        it('should return correctly if core form is not found', async () => {
            coreFormsService.getFormAnswer.mockResolvedValueOnce(null);

            const result = await coreFormsAdapter.getFormAnswer(id);

            expect(result).toBeNull();

            checkMockCallsCount(coreFormsService.getFormAnswer, 1);
        });

        it('should fail if the get operation fails', async () => {
            coreFormsService.getFormAnswer.mockRejectedValueOnce(new Error());

            await expect(coreFormsAdapter.getFormAnswer(id)).rejects.toThrow(Error);
        });
    });

    describe('getFormAnswers', () => {
        let ids: number[];

        beforeEach(() => {
            ids = Array(faker.number.int({ max: 3, min: 1 }))
                .fill(1)
                .map(() => fakePositiveInteger());
        });

        it('should return form answers correctly', async () => {
            coreFormsService.getFormAnswers.mockResolvedValueOnce(
                ids.map((id) => generateFormAnswerWithValues(id, faker.string.uuid(), fakePositiveInteger(), [])),
            );

            const result = await coreFormsAdapter.getFormAnswers(ids);

            expect(result).toHaveLength(ids.length);

            result.forEach((formAnswer, index) => {
                expect(formAnswer).toBeInstanceOf(FormAnswer);
                expect(formAnswer.id).toBe(ids[index]);
            });

            checkMockCallsCount(coreFormsService.getFormAnswers, 1);
        });

        it('should return correctly if empty array', async () => {
            coreFormsService.getFormAnswers.mockResolvedValueOnce([]);

            const result = await coreFormsAdapter.getFormAnswers(ids);

            expect(result).toHaveLength(0);

            checkMockCallsCount(coreFormsService.getFormAnswers, 1);
        });

        it('should fail if the get operation fails', async () => {
            coreFormsService.getFormAnswers.mockRejectedValueOnce(new Error());

            await expect(coreFormsAdapter.getFormAnswers(ids)).rejects.toThrow(Error);
        });
    });

    describe('convertReferenceIdRecordsToQuestionIdRecords', () => {
        let coreForm: CoreForm;
        let referenceIdRecords: { [referenceId: number]: number };

        beforeEach(() => {
            const tag = faker.string.uuid();
            const id = fakePositiveInteger();
            coreForm = generateRandomCoreFormWithQuestions(tag, id);
            referenceIdRecords = coreForm.questionReferenceIds.reduce((records, referenceId) => {
                records[referenceId] = fakePositiveInteger();
                return records;
            }, {} as { [referenceId: number]: number });
        });

        it('should return empty if core form has no questions', () => {
            coreForm.content.steps = [];

            const result = coreFormsAdapter.convertReferenceIdRecordsToQuestionIdRecords(referenceIdRecords, coreForm);
            const keys = Object.keys(result);

            expect(keys).toHaveLength(0);
        });

        it('should return converted references in normal conditions', () => {
            const result = coreFormsAdapter.convertReferenceIdRecordsToQuestionIdRecords(referenceIdRecords, coreForm);
            const coreFormQuestions = coreForm.content.steps.flatMap((step) => step.questions);

            expect(Object.keys(result)).toHaveLength(Object.keys(referenceIdRecords).length);

            coreFormQuestions.forEach((question) => {
                const expectedValue = question.referenceId !== undefined ? referenceIdRecords[question.referenceId] : undefined;

                expect(result[question.id]).toBe(expectedValue);
            });
        });

        it('should skip questions with no reference id', () => {
            coreForm.content.steps
                .flatMap((step) => step.questions)
                .forEach((question) => {
                    question.referenceId = undefined;
                });

            const result = coreFormsAdapter.convertReferenceIdRecordsToQuestionIdRecords(referenceIdRecords, coreForm);
            const keys = Object.keys(result);

            expect(keys).toHaveLength(0);
        });

        it('should skip questions that do not belong to referenceIdRecords', () => {
            referenceIdRecords = {};
            const result = coreFormsAdapter.convertReferenceIdRecordsToQuestionIdRecords(referenceIdRecords, coreForm);
            const coreFormQuestions = coreForm.content.steps.flatMap((step) => step.questions);

            expect(Object.keys(result)).toHaveLength(Object.keys(referenceIdRecords).length);

            coreFormQuestions.forEach((question) => {
                const expectedValue = question.referenceId !== undefined ? referenceIdRecords[question.referenceId] : undefined;

                expect(result[question.id]).toBe(expectedValue);
            });
        });
    });

    describe('calculateAnswerScore', () => {
        let formAnswer: FormAnswer;
        let questionWeights: { [id: string]: number };

        beforeEach(() => {
            formAnswer = generateRandomFormAnswer();
            formAnswer.questionAnswers = Array(faker.number.int({ max: 5, min: 2 }))
                .fill(1)
                .map(() => generateRandomFormQuestionAnswers(formAnswer.id, QUESTION_TYPES.RATING));
            formAnswer.questionAnswers.forEach((questionAnswer) => (questionAnswer.convertedAnswer = parseInt(questionAnswer.answer!, 10)));
            const questionIds = formAnswer.questionAnswers.map((questionAnswer) => questionAnswer.questionId);
            questionWeights = generateQuestionWeightsFromIds(questionIds);
        });

        it('should return correct score under normal conditions', () => {
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);
            const percentageScore = formAnswer.questionAnswers!.reduce((total, curr) => {
                const choiceCount = curr.questionMetadata.choices?.length ?? 1;
                const rating = curr.convertedAnswer as number;
                const score = choiceCount === 1 ? 1 : rating / (choiceCount - 1);
                return total + score * questionWeights[curr.questionId];
            }, 0);

            expect(result).toBe(Math.round(percentageScore));
        });

        it('should return rounded score if it is a decimal value', () => {
            formAnswer.questionAnswers = formAnswer.questionAnswers?.slice(0, 2);
            const sevenChoices = faker.helpers.multiple(faker.word.words, { count: 7 });
            formAnswer.questionAnswers![0].convertedAnswer = 4;
            formAnswer.questionAnswers![0].questionMetadata.choices = sevenChoices;
            formAnswer.questionAnswers![1].convertedAnswer = 4;
            formAnswer.questionAnswers![1].questionMetadata.choices = sevenChoices;
            questionWeights = {
                [formAnswer.questionAnswers![0].questionId]: 33,
                [formAnswer.questionAnswers![1].questionId]: 67,
            };
            const expectedScore = 67; // 71 = 4 * 33 / (7 - 1) + 4 * 67 / (7 - 1)

            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);

            expect(result).toBe(expectedScore);
        });

        it('should skip all non-rating questions', () => {
            formAnswer.questionAnswers?.forEach((questionAnswer) => (questionAnswer.questionMetadata.type = QUESTION_TYPES.MULTIPLE_CHOICE));
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);

            expect(result).toBeNull();
        });

        it('should skip all questions with nullish choices', () => {
            formAnswer.questionAnswers?.forEach((questionAnswer) => (questionAnswer.questionMetadata.choices = null));
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);

            expect(result).toBeNull();
        });

        it('should skip all questions with empty choices', () => {
            formAnswer.questionAnswers?.forEach((questionAnswer) => (questionAnswer.questionMetadata.choices = []));
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);

            expect(result).toBeNull();
        });

        it('should skip all questions with no converted answer', () => {
            formAnswer.questionAnswers?.forEach((questionAnswer) => (questionAnswer.convertedAnswer = undefined));
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights);

            expect(result).toBeNull();
        });

        it('should set weights to 0 if questions have no weight', () => {
            const result = coreFormsAdapter.calculateAnswerScore(formAnswer, {});

            expect(result).toBe(0);
        });

        it('should throw illegal state error if form answer has no question answers', () => {
            formAnswer.questionAnswers = undefined;

            expect(() => coreFormsAdapter.calculateAnswerScore(formAnswer, questionWeights)).toThrow(IllegalStateError);
        });
    });

    describe('calculateGoalScore', () => {
        let goalQuestionsWithAnswer: PerformanceReviewGoalQuestionWithAnswerDataFields[];
        let questionWeights: { [id: string]: number };

        beforeEach(() => {
            goalQuestionsWithAnswer = Array(faker.number.int({ max: 5, min: 2 }))
                .fill(1)
                .map(() => generateGoalQuestionWithAnswer(faker.string.uuid(), fakePositiveInteger(), QUESTION_TYPES.RATING));
            const goalIds = goalQuestionsWithAnswer.map((goalQuestionWithAnswer) => goalQuestionWithAnswer.goalId);
            questionWeights = generateQuestionWeightsFromIds(goalIds);
        });

        it('should return correct score', () => {
            const result = coreFormsAdapter.calculateGoalScore(goalQuestionsWithAnswer, questionWeights);
            const partialScoreMap = {};
            const percentageScore = goalQuestionsWithAnswer.reduce((total, curr) => {
                const choiceCount = curr.questionWithAnswer!.choices?.length ?? 1;
                const rating = curr.questionWithAnswer!.questionAnswer!.answer as number;
                const score = choiceCount === 1 ? 1 : rating / (choiceCount - 1);
                partialScoreMap[curr.goalId] = score;
                return total + score * questionWeights[curr.goalId];
            }, 0);

            expect(result!.score).toBe(Math.round(percentageScore));
            expect(result!.partialGoalScoreMap).toStrictEqual(partialScoreMap);
        });

        it('should skip all non-rating questions', () => {
            goalQuestionsWithAnswer.forEach((formQuestionAnswer) => (formQuestionAnswer.questionWithAnswer!.type = QUESTION_TYPES.MULTIPLE_CHOICE));
            const result = coreFormsAdapter.calculateGoalScore(goalQuestionsWithAnswer, questionWeights);

            expect(result.score).toBeNull();
            expect(result.partialGoalScoreMap).toStrictEqual({});
        });

        it('should skip all questions with nullish choices', () => {
            goalQuestionsWithAnswer.forEach((formQuestionAnswer) => (formQuestionAnswer.questionWithAnswer!.choices = null));
            const result = coreFormsAdapter.calculateGoalScore(goalQuestionsWithAnswer, questionWeights);

            expect(result.score).toBeNull();
            expect(result.partialGoalScoreMap).toStrictEqual({});
        });

        it('should skip all questions with empty choices', () => {
            goalQuestionsWithAnswer.forEach((formQuestionAnswer) => (formQuestionAnswer.questionWithAnswer!.choices = []));
            const result = coreFormsAdapter.calculateGoalScore(goalQuestionsWithAnswer, questionWeights);

            expect(result.score).toBeNull();
            expect(result.partialGoalScoreMap).toStrictEqual({});
        });

        it('should skip all questions with no answer', () => {
            goalQuestionsWithAnswer.forEach((formQuestionAnswer) => (formQuestionAnswer.questionWithAnswer!.questionAnswer!.answer = undefined));
            const result = coreFormsAdapter.calculateGoalScore(goalQuestionsWithAnswer, questionWeights);

            expect(result.score).toBeNull();
            expect(result.partialGoalScoreMap).toStrictEqual({});
        });
    });

    describe('generateAnswerString', () => {
        it('should generate answer', () => {
            const answer = faker.string.alpha(10);
            coreFormsService.generateAnswerString.mockImplementationOnce(() => answer);
            const result = coreFormsAdapter.generateAnswerString(generateRandomCoreFormQuestion(), faker.string.alpha(10));

            expect(result).not.toBeNull();
            expect(result).toBe(answer);

            checkMockCallsCount(coreFormsService.generateAnswerString, 1);
        });
    });

    describe('generateCommentString', () => {
        it('should generate comment', () => {
            const comment = faker.string.alpha(10);
            coreFormsService.generateCommentString.mockImplementationOnce(() => comment);
            const result = coreFormsAdapter.generateCommentString(generateRandomCoreFormQuestion(), faker.string.alpha(10));

            expect(result).not.toBeNull();
            expect(result).toBe(comment);

            checkMockCallsCount(coreFormsService.generateCommentString, 1);
        });
    });

    describe('validateQuestionRequired', () => {
        it('should validate question', () => {
            expect(() =>
                coreFormsAdapter.validateQuestionRequired(generateRandomCoreFormQuestion(), faker.string.alpha(10), faker.string.alpha(10)),
            ).not.toThrow();

            checkMockCallsCount(coreFormsService.validateQuestionRequired, 1);
        });
    });
});
