import { mock, MockProxy } from 'jest-mock-extended';
import { LogicalOperator } from '../../../../src/api/modules/audiences/business/constants/logicalOperators';
import { SegmentationType } from '../../../../src/api/modules/audiences/business/constants/segmentationTypes';
import { SegmentationExpressionCondition } from '../../../../src/api/modules/audiences/business/interfaces/segmentationExpressionConditions';
import { SEGMENTABLE_TYPES } from '../../../../src/api/modules/audiences/business/models/segmentableEntityType';
import { AudiencesService } from '../../../../src/api/modules/audiences/business/services/audiencesService';
import { SegmentationItem } from '../../../../src/api/modules/segmentations/business/models/segmentationItem';
import { SegmentationsService } from '../../../../src/api/modules/segmentations/business/services/segmentationsService';
import { PerformanceReviewCalibrationRule } from '../../../../src/api/modules/performanceReviews/business/models/performanceReviewCalibrationRule';
import { PerformanceReviewCalibrationAudiencesAdapter } from '../../../../src/api/modules/performanceReviews/infrastructure/adapters/performanceReviewCalibrationAudiencesAdapter';
import { fakePositiveInteger } from '../../../common/common';

const buildSegmentationItem = (id: number, groupId: number): SegmentationItem => {
    const item = new SegmentationItem();
    item.setId(id);
    item.setSegmentationGroupId(groupId);
    item.setName(`item-${id}`);
    return item;
};

describe('PerformanceReviewCalibrationAudiencesAdapter', () => {
    let adapter: PerformanceReviewCalibrationAudiencesAdapter;
    let mockAudiencesService: MockProxy<AudiencesService>;
    let mockSegmentationsService: MockProxy<SegmentationsService>;

    beforeEach(() => {
        mockAudiencesService = mock<AudiencesService>();
        mockSegmentationsService = mock<SegmentationsService>();
        adapter = new PerformanceReviewCalibrationAudiencesAdapter(mockAudiencesService, mockSegmentationsService);
    });

    afterEach(() => {
        jest.clearAllMocks();
    });

    describe('getCalibratorUserIds', () => {
        it('should filter out deleted items and still call getUsersAudience with remaining items', async () => {
            const instanceId = fakePositiveInteger();
            const existingItemId = fakePositiveInteger();
            const deletedItemId = fakePositiveInteger();
            const groupId = fakePositiveInteger();
            const existingItem = buildSegmentationItem(existingItemId, groupId);
            const userId = fakePositiveInteger();

            const conditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [existingItemId, deletedItemId] },
            ];

            mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([existingItem]);
            mockAudiencesService.getUsersAudience.mockResolvedValue({ count: 1, userIds: [userId] });

            const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

            expect(result).toEqual({ count: 1, userIds: [userId] });
            expect(mockSegmentationsService.getSegmentationItemsByIdsOrFail).not.toHaveBeenCalled();
            expect(mockAudiencesService.getUsersAudience).toHaveBeenCalledTimes(1);
        });

        it('should return empty result when all segmentation items are deleted', async () => {
            const instanceId = fakePositiveInteger();
            const deletedItemId1 = fakePositiveInteger();
            const deletedItemId2 = fakePositiveInteger();

            const conditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [deletedItemId1, deletedItemId2] },
            ];

            mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([]);

            const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

            expect(result).toEqual({ count: 0, userIds: [] });
            expect(mockAudiencesService.getUsersAudience).not.toHaveBeenCalled();
        });

        it('should resolve users when all items are found', async () => {
            const instanceId = fakePositiveInteger();
            const itemId = fakePositiveInteger();
            const groupId = fakePositiveInteger();
            const item = buildSegmentationItem(itemId, groupId);
            const userId = fakePositiveInteger();

            const conditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [itemId] },
            ];

            mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([item]);
            mockAudiencesService.getUsersAudience.mockResolvedValue({ count: 1, userIds: [userId] });

            const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

            expect(result).toEqual({ count: 1, userIds: [userId] });
            expect(mockAudiencesService.getUsersAudience).toHaveBeenCalledTimes(1);
            expect(mockSegmentationsService.getSegmentationItemsByIdsOrFail).not.toHaveBeenCalled();
        });

        it('should use only bestEffort (never OrFail) for item resolution', async () => {
            const instanceId = fakePositiveInteger();
            const itemId = fakePositiveInteger();
            const groupId = fakePositiveInteger();
            const item = buildSegmentationItem(itemId, groupId);

            const conditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [itemId] },
            ];

            mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([item]);
            mockAudiencesService.getUsersAudience.mockResolvedValue({ count: 0, userIds: [] });

            await adapter.getCalibratorUserIds(instanceId, conditions, 6);

            expect(mockSegmentationsService.getSegmentationItemsByIdsBestEffort).toHaveBeenCalledWith([itemId], instanceId);
            expect(mockSegmentationsService.getSegmentationItemsByIdsOrFail).not.toHaveBeenCalled();
        });

        describe('EXPRESSION conditions (nested)', () => {
            it('should keep EXPRESSION condition when its nested expressions survive filtering', async () => {
                const instanceId = fakePositiveInteger();
                const existingItemId = fakePositiveInteger();
                const deletedItemId = fakePositiveInteger();
                const groupId = fakePositiveInteger();
                const existingItem = buildSegmentationItem(existingItemId, groupId);
                const userId = fakePositiveInteger();

                // EXPRESSION wrapping two nested ITEMS, one of which is deleted
                const conditions: SegmentationExpressionCondition[] = [
                    {
                        type: SegmentationType.EXPRESSION,
                        operator: LogicalOperator.OR,
                        expressions: [
                            { type: SegmentationType.ITEMS, segmentationItemIds: [existingItemId] },
                            { type: SegmentationType.ITEMS, segmentationItemIds: [deletedItemId] },
                        ],
                    },
                ];

                mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([existingItem]);
                mockAudiencesService.getUsersAudience.mockResolvedValue({ count: 1, userIds: [userId] });

                const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

                expect(result).toEqual({ count: 1, userIds: [userId] });
                expect(mockAudiencesService.getUsersAudience).toHaveBeenCalledTimes(1);
            });

            it('should remove EXPRESSION condition when all nested items are deleted', async () => {
                const instanceId = fakePositiveInteger();
                const deletedItemId1 = fakePositiveInteger();
                const deletedItemId2 = fakePositiveInteger();

                const conditions: SegmentationExpressionCondition[] = [
                    {
                        type: SegmentationType.EXPRESSION,
                        operator: LogicalOperator.OR,
                        expressions: [
                            { type: SegmentationType.ITEMS, segmentationItemIds: [deletedItemId1] },
                            { type: SegmentationType.ITEMS, segmentationItemIds: [deletedItemId2] },
                        ],
                    },
                ];

                mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([]);

                const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

                expect(result).toEqual({ count: 0, userIds: [] });
                expect(mockAudiencesService.getUsersAudience).not.toHaveBeenCalled();
            });

            it('should keep EXPRESSION condition that has only root segmentationItemIds (no expressions) when items exist', async () => {
                const instanceId = fakePositiveInteger();
                const existingItemId = fakePositiveInteger();
                const groupId = fakePositiveInteger();
                const existingItem = buildSegmentationItem(existingItemId, groupId);
                const userId = fakePositiveInteger();

                const conditions: SegmentationExpressionCondition[] = [
                    {
                        type: SegmentationType.EXPRESSION,
                        operator: LogicalOperator.OR,
                        segmentationItemIds: [existingItemId],
                    },
                ];

                mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([existingItem]);
                mockAudiencesService.getUsersAudience.mockResolvedValue({ count: 1, userIds: [userId] });

                const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

                expect(result).toEqual({ count: 1, userIds: [userId] });
                expect(mockAudiencesService.getUsersAudience).toHaveBeenCalledTimes(1);
            });

            it('should remove EXPRESSION condition that has only root segmentationItemIds when all items are deleted', async () => {
                const instanceId = fakePositiveInteger();
                const deletedItemId = fakePositiveInteger();

                const conditions: SegmentationExpressionCondition[] = [
                    {
                        type: SegmentationType.EXPRESSION,
                        operator: LogicalOperator.OR,
                        segmentationItemIds: [deletedItemId],
                    },
                ];

                mockSegmentationsService.getSegmentationItemsByIdsBestEffort.mockResolvedValue([]);

                const result = await adapter.getCalibratorUserIds(instanceId, conditions, 6);

                expect(result).toEqual({ count: 0, userIds: [] });
                expect(mockAudiencesService.getUsersAudience).not.toHaveBeenCalled();
            });
        });
    });

    describe('getCollaboratorUserIds', () => {
        it('should use strict validation (getSegmentationItemsByIdsOrFail) and not bestEffort', async () => {
            const instanceId = fakePositiveInteger();
            const itemId = fakePositiveInteger();
            const groupId = fakePositiveInteger();
            const item = buildSegmentationItem(itemId, groupId);

            const conditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [itemId] },
            ];

            mockSegmentationsService.getSegmentationItemsByIdsOrFail.mockResolvedValue([item]);
            mockAudiencesService.getAllTargetedUserIds.mockResolvedValue([]);

            await adapter.getCollaboratorUserIds(instanceId, conditions);

            expect(mockSegmentationsService.getSegmentationItemsByIdsOrFail).toHaveBeenCalledWith([itemId], instanceId);
            expect(mockSegmentationsService.getSegmentationItemsByIdsBestEffort).not.toHaveBeenCalled();
        });
    });

    describe('upsertEntity', () => {
        it('should upsert segmentable entity using calibrated audience conditions when entityType is calibrated', async () => {
            const instanceId = fakePositiveInteger();
            const itemId = fakePositiveInteger();
            const groupId = fakePositiveInteger();
            const item = buildSegmentationItem(itemId, groupId);

            const calibratedConditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [itemId] },
            ];
            const calibratorsConditions: SegmentationExpressionCondition[] = [
                { type: SegmentationType.ITEMS, segmentationItemIds: [fakePositiveInteger()] },
            ];

            const rule = new PerformanceReviewCalibrationRule({
                id: '018f1234-5678-7abc-def0-123456789abc',
                instanceId,
                cycleId: fakePositiveInteger(),
                calibratorsAudience: { conditions: calibratorsConditions },
                calibratedAudience: { conditions: calibratedConditions },
                createdAt: new Date(),
                updatedAt: new Date(),
            });

            mockSegmentationsService.getSegmentationItemsByIdsOrFail.mockResolvedValue([item]);
            mockAudiencesService.upsertEntity.mockResolvedValue(undefined as any);

            await adapter.upsertEntity(rule, SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED);

            expect(mockSegmentationsService.getSegmentationItemsByIdsOrFail).toHaveBeenCalledWith([itemId], instanceId);
            expect(mockAudiencesService.upsertEntity).toHaveBeenCalledWith(
                expect.objectContaining({
                    entityId: rule.id,
                    instanceId: rule.instanceId,
                    type: SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED,
                    segmentationPayload: { segmentationConfigs: expect.any(Array) },
                }),
            );
        });
    });

    describe('deleteEntity', () => {
        it('should delete calibrated segmentable entity by rule id when entityType is calibrated', async () => {
            const instanceId = fakePositiveInteger();
            mockAudiencesService.deleteEntityById.mockResolvedValue(undefined);

            await adapter.deleteEntity('some-rule-id', instanceId, SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED);

            expect(mockAudiencesService.deleteEntityById).toHaveBeenCalledWith({
                entityId: 'some-rule-id',
                type: SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED,
                instanceId,
            });
        });
    });

    describe('getRuleIdsWhereUserIs', () => {
        it('should return entity ids from audiences service using calibrated type when entityType is calibrated', async () => {
            mockAudiencesService.getAllSegmentableEntityIdsByUserIdCursorPaginated.mockResolvedValue({
                ids: [],
                entityIds: ['rule-1', 'rule-2'],
            });

            const result = await adapter.getRuleIdsWhereUserIs(42, 7, SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED);

            expect(result).toEqual(['rule-1', 'rule-2']);
            expect(mockAudiencesService.getAllSegmentableEntityIdsByUserIdCursorPaginated).toHaveBeenCalledWith(
                42,
                7,
                SEGMENTABLE_TYPES.CALIBRATION_RULE_CALIBRATED,
            );
        });
    });
});
