import { MetadataTemplateFieldType } from '@box/metadata-editor';
import { renderHook, waitFor } from '../../../test-utils/testing-library';
import messages from '../../common/messages';
import {
    ERROR_CODE_EMPTY_METADATA_SUGGESTIONS,
    ERROR_CODE_FETCH_METADATA_SUGGESTIONS,
    ERROR_CODE_METADATA_AUTOFILL_TIMEOUT,
    ERROR_CODE_METADATA_PRECONDITION_FAILED,
    ERROR_CODE_UNKNOWN,
    FIELD_PERMISSIONS_CAN_UPLOAD,
    SUCCESS_CODE_DELETE_METADATA_TEMPLATE_INSTANCE,
    SUCCESS_CODE_UPDATE_METADATA_TEMPLATE_INSTANCE,
    SUCCESS_CODE_CREATE_METADATA_TEMPLATE_INSTANCE,
} from '../../../constants';
import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher';

const mockRateLimitError = {
    status: 429,
    message: 'Rate Limit Exceeded',
};

const mockInternalServerError = {
    status: 500,
    message: 'Internal Server Error',
};

const mockTimeoutError = {
    status: 408,
    message: 'Request Timeout',
};

const mockPreconditionFailedError = {
    status: 412,
    message: 'Precondition Failed',
};

const mockFile = {
    id: '123',
    permissions: { [FIELD_PERMISSIONS_CAN_UPLOAD]: true },
    type: 'file',
};

const mockTemplates = [
    {
        canEdit: true,
        id: 'metadata_template_instance_1',
        fields: [
            {
                key: 'field1',
                type: 'string' as MetadataTemplateFieldType,
                hidden: false,
            },
            {
                key: 'field2',
                type: 'string' as MetadataTemplateFieldType,
                hidden: false,
            },
        ],
        scope: 'global',
        templateKey: 'templateKey',
    },
    {
        id: 'metadata_template_custom_1',
        scope: 'global',
        templateKey: 'properties',
        hidden: false,
    },
];

const mockTemplateInstances = [
    {
        canEdit: true,
        id: 'metadata_template_instance_2',
        fields: [],
        scope: 'global',
        templateKey: 'properties1',
        type: 'properties',
        hidden: false,
    },
];

const newTemplateInstance = {
    canEdit: true,
    id: 'metadata_template_instance_3',
    fields: [],
    scope: 'global',
    templateKey: 'properties',
    type: 'properties',
    hidden: false,
};

const mockAPI = {
    getFile: jest.fn((id, successCallback, errorCallback) => {
        try {
            successCallback(mockFile);
        } catch (error) {
            errorCallback(error);
        }
    }),
    getMetadata: jest.fn((_file, successCallback, errorCallback) => {
        try {
            successCallback({
                editors: [],
                templates: mockTemplates,
                templateInstances: mockTemplateInstances,
            });
        } catch (error) {
            errorCallback(error);
        }
    }),
    deleteMetadata: jest.fn((_file, template, successCallback, errorCallback) => {
        try {
            successCallback(template);
        } catch (error) {
            errorCallback(error);
        }
    }),
    createMetadataRedesign: jest.fn((_file, template, successCallback, errorCallback) => {
        try {
            successCallback();
        } catch (error) {
            errorCallback(error);
        }
    }),
    updateMetadataRedesign: jest.fn((_file, _metadataInstance, _JSONPatch, successCallback, errorCallback) => {
        try {
            successCallback();
        } catch (error) {
            errorCallback(error);
        }
    }),
    extractStructured: jest.fn(),
};
const api = {
    getFileAPI: jest.fn().mockReturnValue(mockAPI),
    getMetadataAPI: jest.fn().mockReturnValue(mockAPI),
    getIntelligenceAPI: jest.fn().mockReturnValue(mockAPI),
};

describe('useSidebarMetadataFetcher', () => {
    const onErrorMock = jest.fn();
    const onSuccessMock = jest.fn();
    const isFeatureEnabledMock = true;

    const setupHook = (fileId = '123') =>
        renderHook(() => useSidebarMetadataFetcher(api, fileId, onErrorMock, onSuccessMock, isFeatureEnabledMock));

    beforeEach(() => {
        onErrorMock.mockClear();
        onSuccessMock.mockClear();
        mockAPI.getFile.mockClear();
        mockAPI.getMetadata.mockClear();
        mockAPI.deleteMetadata.mockClear();
        mockAPI.updateMetadataRedesign.mockClear();
        mockAPI.extractStructured.mockClear();
    });

    test('should fetch the file and metadata successfully', async () => {
        const { result } = setupHook();

        await waitFor(() => expect(result.current.status).toBe(STATUS.SUCCESS));

        expect(result.current.file).toEqual(mockFile);
        expect(result.current.templates).toEqual(mockTemplates);
        expect(result.current.errorMessage).toBeNull();
    });

    test('should handle file fetching error', async () => {
        mockAPI.getFile.mockImplementation((id, successCallback, errorCallback) =>
            errorCallback(mockInternalServerError, 'file_fetch_error'),
        );

        const { result } = setupHook();

        await waitFor(() => expect(result.current.status).toBe(STATUS.ERROR));

        expect(result.current.file).toBeUndefined();
        expect(result.current.errorMessage).toBe(messages.sidebarMetadataEditingErrorContent);
        expect(onSuccessMock).not.toHaveBeenCalled();
        expect(onErrorMock).toHaveBeenCalledWith(
            mockInternalServerError,
            'file_fetch_error',
            expect.objectContaining({
                error: mockInternalServerError,
                isErrorDisplayed: true,
            }),
        );
    });

    test('should handle metadata fetching error', async () => {
        mockAPI.getFile.mockImplementation((id, successCallback) => {
            successCallback(mockFile);
        });
        mockAPI.getMetadata.mockImplementation((file, successCallback, errorCallback) => {
            errorCallback(mockInternalServerError, 'metadata_fetch_error');
        });
        const { result } = setupHook();

        await waitFor(() => expect(result.current.status).toBe(STATUS.ERROR));

        expect(result.current.templates).toBeNull();
        expect(result.current.errorMessage).toBe(messages.sidebarMetadataFetchingErrorContent);
        expect(onSuccessMock).not.toHaveBeenCalled();
        expect(onErrorMock).toHaveBeenCalledWith(
            mockInternalServerError,
            'metadata_fetch_error',
            expect.objectContaining({
                error: mockInternalServerError,
                isErrorDisplayed: true,
            }),
        );
    });

    test('should handle metadata instance removal', async () => {
        mockAPI.getMetadata.mockImplementation((file, successCallback) => {
            successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates });
        });
        mockAPI.deleteMetadata.mockImplementation((file, template, successCallback) => {
            successCallback(mockTemplateInstances[0]);
        });

        const { result } = setupHook();
        expect(result.current.templateInstances).toEqual(mockTemplateInstances);

        await waitFor(() => result.current.handleDeleteMetadataInstance(mockTemplateInstances[0]));

        expect(result.current.templates).toEqual(mockTemplates);
        expect(result.current.status).toEqual(STATUS.SUCCESS);
        expect(result.current.errorMessage).toBeNull();
        expect(onSuccessMock).toHaveBeenCalledWith(SUCCESS_CODE_DELETE_METADATA_TEMPLATE_INSTANCE, true);
    });

    test('should handle metadata instance removal error', async () => {
        mockAPI.getMetadata.mockImplementation((file, successCallback) => {
            successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates });
        });
        mockAPI.deleteMetadata.mockImplementation((file, template, successCallback, errorCallback) => {
            errorCallback(mockInternalServerError, 'metadata_remove_error');
        });

        const { result } = setupHook();
        expect(result.current.status).toEqual(STATUS.SUCCESS);

        await waitFor(() => result.current.handleDeleteMetadataInstance(mockTemplateInstances[0]));

        expect(result.current.status).toEqual(STATUS.ERROR);
        expect(onSuccessMock).not.toHaveBeenCalled();
        expect(onErrorMock).toHaveBeenCalledWith(
            mockInternalServerError,
            'metadata_remove_error',
            expect.objectContaining({
                error: mockInternalServerError,
                isErrorDisplayed: true,
            }),
        );
    });

    test('should handle metadata instance creation', async () => {
        mockAPI.getMetadata.mockImplementation((file, successCallback) => {
            successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates });
        });
        mockAPI.createMetadataRedesign.mockImplementation((file, template, successCallback) => {
            successCallback();
        });

        const successCallback = jest.fn();

        const { result } = setupHook();

        expect(result.current.templateInstances).toEqual(mockTemplateInstances);
        await waitFor(() => result.current.handleCreateMetadataInstance(newTemplateInstance, successCallback));

        expect(successCallback).toHaveBeenCalled();
        expect(onSuccessMock).toHaveBeenCalledWith(SUCCESS_CODE_CREATE_METADATA_TEMPLATE_INSTANCE, true);
    });

    test('should handle metadata instance creation error', async () => {
        mockAPI.getMetadata.mockImplementation((file, successCallback) => {
            successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates });
        });
        mockAPI.createMetadataRedesign.mockImplementation((file, template, successCallback, errorCallback) => {
            errorCallback(mockInternalServerError, 'metadata_creation_error');
        });

        const { result } = setupHook();
        expect(result.current.status).toBe(STATUS.SUCCESS);

        await waitFor(() => result.current.handleCreateMetadataInstance(newTemplateInstance, jest.fn()));

        expect(result.current.status).toBe(STATUS.ERROR);
        expect(onSuccessMock).not.toHaveBeenCalled();
        expect(onErrorMock).toHaveBeenCalledWith(
            mockInternalServerError,
            'metadata_creation_error',
            expect.objectContaining({
                error: mockInternalServerError,
                isErrorDisplayed: true,
            }),
        );
    });

    test('should handle metadata update', async () => {
        mockAPI.getMetadata.mockImplementation((file, successCallback) => {
            successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates });
        });
        mockAPI.updateMetadataRedesign.mockImplementation((_file, _metadataInstance, _JSONPatch, successCallback) => {
            successCallback();
        });
        const ops = [{ op: 'add', path: '/foo', value: 'bar' }];
        const successCallback = jest.fn();

        const { result } = setupHook();
        expect(result.current.templateInstances).toEqual(mockTemplateInstances);

        await waitFor(() =>
            result.current.handleUpdateMetadataInstance(mockTemplateInstances[0], ops, successCallback),
        );
        expect(successCallback).toHaveBeenCalled();
        expect(onSuccessMock).toHaveBeenCalledWith(SUCCESS_CODE_UPDATE_METADATA_TEMPLATE_INSTANCE, true);
    });

    test('should handle metadata update error', async () => {
        mockAPI.updateMetadataRedesign.mockImplementation(
            (_file, _metadataInstance, _JSONPatch, successCallback, errorCallback) => {
                errorCallback(mockInternalServerError, 'metadata_update_error');
            },
        );
        const ops = [{ op: 'add', path: '/foo', value: 'bar' }];
        const successCallback = jest.fn();
        const { result } = setupHook();

        expect(result.current.templateInstances).toEqual(mockTemplateInstances);

        await waitFor(() =>
            result.current.handleUpdateMetadataInstance(mockTemplateInstances[0], ops, successCallback),
        );

        expect(successCallback).not.toHaveBeenCalled();
        expect(onSuccessMock).not.toHaveBeenCalled();

        expect(result.current.status).toEqual(STATUS.ERROR);
        expect(result.current.templates).toEqual(mockTemplates);
        expect(result.current.errorMessage).toEqual(messages.sidebarMetadataEditingErrorContent);
        expect(onErrorMock).toHaveBeenCalledWith(
            mockInternalServerError,
            'metadata_update_error',
            expect.objectContaining({
                error: mockInternalServerError,
                isErrorDisplayed: true,
            }),
        );
    });

    describe('extractSuggestions', () => {
        test('should extract suggestions successfully', async () => {
            const mockSuggestions = {
                field1: 'value1',
                field2: 'value2',
            };
            mockAPI.extractStructured.mockResolvedValue(mockSuggestions);

            const { result } = setupHook();

            expect(result.current.templates).toEqual(mockTemplates);

            const suggestions = await result.current.extractSuggestions('templateKey', 'global');

            expect(suggestions).toEqual([
                { ...mockTemplates[0].fields[0], aiSuggestion: 'value1' },
                { ...mockTemplates[0].fields[1], aiSuggestion: 'value2' },
            ]);
        });

        test('should map taxonomy field suggestions with value and displayValue', async () => {
            const taxonomyTemplate = {
                canEdit: true,
                id: 'metadata_template_taxonomy',
                fields: [
                    {
                        key: 'taxonomyField',
                        type: 'taxonomy' as MetadataTemplateFieldType,
                        hidden: false,
                    },
                ],
                scope: 'global',
                templateKey: 'taxonomyTemplateKey',
            };

            const mockTaxonomySuggestions = {
                taxonomyField: [
                    { id: 'taxonomy-id-1', displayName: 'Taxonomy Item 1' },
                    { id: 'taxonomy-id-2', displayName: 'Taxonomy Item 2' },
                ],
            };

            mockAPI.getMetadata.mockImplementation((file, successCallback) => {
                successCallback({
                    editors: [],
                    templates: [taxonomyTemplate],
                    templateInstances: [],
                });
            });
            mockAPI.extractStructured.mockResolvedValue(mockTaxonomySuggestions);

            const { result } = setupHook();

            await waitFor(() => expect(result.current.templates).toEqual([taxonomyTemplate]));

            const suggestions = await result.current.extractSuggestions('taxonomyTemplateKey', 'global');

            expect(suggestions).toEqual([
                {
                    ...taxonomyTemplate.fields[0],
                    aiSuggestion: [
                        { value: 'taxonomy-id-1', displayValue: 'Taxonomy Item 1' },
                        { value: 'taxonomy-id-2', displayValue: 'Taxonomy Item 2' },
                    ],
                },
            ]);
        });

        test('should handle user correctable error during suggestions extraction', async () => {
            mockAPI.extractStructured.mockRejectedValue({ response: mockRateLimitError });

            const { result } = setupHook();
            const suggestions = await result.current.extractSuggestions('templateKey', 'global');

            expect(suggestions).toEqual([]);
            expect(onSuccessMock).not.toHaveBeenCalled();
            expect(onErrorMock).toHaveBeenCalledWith(
                { response: mockRateLimitError },
                ERROR_CODE_FETCH_METADATA_SUGGESTIONS,
                expect.objectContaining({
                    showNotification: true,
                }),
            );
            await waitFor(() => expect(result.current.extractErrorCode).toBeNull());
        });

        test.each`
            description                               | error                                        | expectedErrorCode
            ${'metadata autofill timeout error'}      | ${{ response: mockTimeoutError }}            | ${ERROR_CODE_METADATA_AUTOFILL_TIMEOUT}
            ${'metadata pre-condition failure error'} | ${{ response: mockPreconditionFailedError }} | ${ERROR_CODE_METADATA_PRECONDITION_FAILED}
            ${'internal server error'}                | ${{ response: mockInternalServerError }}     | ${ERROR_CODE_UNKNOWN}
        `('should set extract error code for $description and get cleared', async ({ error, expectedErrorCode }) => {
            mockAPI.extractStructured.mockRejectedValue(error);

            const { result } = setupHook();
            const suggestions = await result.current.extractSuggestions('templateKey', 'global');

            expect(suggestions).toEqual([]);
            expect(onSuccessMock).not.toHaveBeenCalled();
            expect(onErrorMock).toHaveBeenCalledWith(error, expectedErrorCode);
            await waitFor(() => expect(result.current.extractErrorCode).toEqual(expectedErrorCode));

            await result.current.clearExtractError();
            await waitFor(() => expect(result.current.extractErrorCode).toBeNull());
        });

        test('should handle empty suggestions', async () => {
            mockAPI.extractStructured.mockResolvedValue([]);

            const { result } = setupHook();
            const suggestions = await result.current.extractSuggestions('templateKey', 'global');

            expect(suggestions).toEqual([]);
            expect(onSuccessMock).not.toHaveBeenCalled();
            expect(onErrorMock).toHaveBeenCalledWith(
                new Error('No suggestions found.'),
                ERROR_CODE_EMPTY_METADATA_SUGGESTIONS,
                expect.objectContaining({
                    showNotification: true,
                }),
            );
        });

        test('should call extractStructured with custom AI agent ID', async () => {
            const { result } = setupHook();
            const agentId = 'custom-agent-123';

            await result.current.extractSuggestions('templateKey', 'global', agentId);

            expect(mockAPI.extractStructured).toHaveBeenCalledWith({
                items: [{ id: mockFile.id, type: mockFile.type }],
                metadata_template: { template_key: 'templateKey', scope: 'global', type: 'metadata_template' },
                ai_agent: { type: 'ai_agent_id', id: agentId },
            });
        });

        test('should not call extractStructured with custom AI agent ID', async () => {
            const { result } = setupHook();

            await result.current.extractSuggestions('templateKey', 'global');

            // Assert that ai_agent is NOT present
            expect(mockAPI.extractStructured).toHaveBeenCalledWith(
                expect.not.objectContaining({
                    ai_agent: expect.anything(),
                }),
            );
            // Also verify what IS present
            expect(mockAPI.extractStructured).toHaveBeenCalledWith({
                items: [{ id: mockFile.id, type: mockFile.type }],
                metadata_template: { template_key: 'templateKey', scope: 'global', type: 'metadata_template' },
            });
        });

        test('should handle undefined agentIDs', async () => {
            const { result } = setupHook();

            await result.current.extractSuggestions('templateKey', 'global', undefined);

            // Assert that ai_agent is NOT present
            expect(mockAPI.extractStructured).toHaveBeenCalledWith(
                expect.not.objectContaining({
                    ai_agent: expect.anything(),
                }),
            );
            // Also verify what IS present
            expect(mockAPI.extractStructured).toHaveBeenCalledWith({
                items: [{ id: mockFile.id, type: mockFile.type }],
                metadata_template: { template_key: 'templateKey', scope: 'global', type: 'metadata_template' },
            });
            mockAPI.extractStructured.mockClear();

            await result.current.extractSuggestions('templateKey', 'global', '');
            expect(mockAPI.extractStructured).toHaveBeenCalledWith(
                expect.not.objectContaining({
                    ai_agent: expect.anything(),
                }),
            );
        });
    });
});
