import React from 'react';
import {
    ApolloCache,
    ApolloError,
    DefaultContext,
    FetchResult,
    MutationFunctionOptions,
} from '@apollo/client';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import * as suiteFrontend from '@theorchard/suite-frontend';
import { Identity } from '@theorchard/suite-frontend';
import { createIdentity, renderInAppContext } from '@theorchard/suite-testing';
import paymentAccounts from 'src/__fixtures__/graphql/payment-group-payment-accounts-response.json';
import * as paymentGroupPaymentAccountMutation from 'src/apollo/mutations/payment-group-payment-account';
import * as paymentGroupPaymentAccountQuery from 'src/apollo/queries/payment-group-payment-account';
import { filename } from 'src/components/payment-group-detail/payment-group-payment-accounts-table';
import {
    CONFIRM_FUNDS,
    DEFAULT_ITEMS_PER_PAGE,
    PAYMENT_ACCOUNT_DELETE_MSG,
    PAYMENT_APPROVAL_WARNING_MSG,
} from 'src/constants';
import { smartFormatter } from 'src/utils/amount-helpers';
import * as exportCsvUtil from 'src/utils/export-csv';
import PaymentGroupPaymentAccountsTable, {
    PaymentAccountsTableProps,
} from '../payment-group-payment-accounts-table';
import type {
    SoftDeletePaymentGroupPaymentAccountMutation,
    SoftDeletePaymentGroupPaymentAccountMutationVariables,
} from 'src/apollo/mutations/payment-group-payment-account/__generated__/soft-delete-payment-group-payment-account';
import type { GetPaymentGroupPaymentAccountsQuery } from 'src/apollo/queries/payment-group-payment-account/__generated__/get-payment-group-payment-accounts';

jest.mock('@theorchard/suite-frontend', () => ({
    ...jest.requireActual('@theorchard/suite-frontend'),
    useFeatureFlag: jest.fn().mockReturnValue(true),
}));

jest.mock('react-router', () => ({
    ...jest.requireActual('react-router'),
    useLocation: jest.fn().mockImplementation(() => {
        return { search: 'searchTerm=Test+Search+Term', pathname: '/' };
    }),
}));

describe('<PaymentGroupPaymentAccountsTABLE />', () => {
    const actionStates = {
        approve: { actionName: 'approve', actionStatus: 'init' },
        generate_payments: {
            actionName: 'generate_payments',
            actionStatus: 'complete',
        },
        post: { actionName: 'post', actionStatus: 'init' },
        send_payments: { actionName: 'send_payments', actionStatus: 'init' },
    } as unknown as PaymentAccountsTableProps['actionStates'];

    const render = (
        props: PaymentAccountsTableProps,
        mockIdentity?: Partial<Identity>
    ) =>
        renderInAppContext(<PaymentGroupPaymentAccountsTable {...props} />, {
            identity: createIdentity(mockIdentity),
        });

    const deleteMock = jest.fn().mockResolvedValue({
        data: { abacusSoftDeletePaymentGroupPaymentAccount: { deleted: true } },
    });
    const { items } = paymentAccounts.abacusPaymentGroupPaymentAccounts;
    const componentProps = {
        abacusEvents: [],
        actionStates,
        setErrors: jest.fn(),
        statementPeriod: {
            statementPeriodId: '270',
            statementPeriodName: 'June 2021',
        },
    };
    const updateMock = jest.fn().mockResolvedValue({
        data: {
            abacusUpdatePaymentGroupPaymentAccount: { items, totalCount: 2 },
        },
    });
    let deleteSpy: jest.SpyInstance<
        (
            options?:
                | MutationFunctionOptions<
                      SoftDeletePaymentGroupPaymentAccountMutation,
                      SoftDeletePaymentGroupPaymentAccountMutationVariables,
                      DefaultContext,
                      ApolloCache<any>
                  >
                | undefined
        ) => Promise<
            FetchResult<
                SoftDeletePaymentGroupPaymentAccountMutation,
                Record<string, any>,
                Record<string, any>
            >
        >,
        [listRequestParams: object, deletedPaymentAccountCurrency: any]
    >;
    let requestSpy: jest.SpyInstance<
        {
            data: GetPaymentGroupPaymentAccountsQuery | undefined;
            error: ApolloError | undefined;
            loading: boolean;
        },
        [variables: any]
    >;
    let updateSpy: jest.SpyInstance;

    afterEach(jest.restoreAllMocks);
    beforeEach(() => {
        deleteSpy = jest
            .spyOn(
                paymentGroupPaymentAccountMutation,
                'useDeletePaymentGroupPaymentAccount'
            )
            .mockReturnValue(deleteMock);
        requestSpy = jest
            .spyOn(
                paymentGroupPaymentAccountQuery,
                'usePaymentGroupPaymentAccounts'
            )
            .mockReturnValue({
                data: paymentAccounts,
                loading: false,
                error: undefined,
            });
        updateSpy = jest
            .spyOn(
                paymentGroupPaymentAccountMutation,
                'useUpdatePaymentGroupPaymentAccount'
            )
            .mockReturnValue({
                updatePaymentGroupPaymentAccount: updateMock,
                loading: false,
            });
    });

    it('requests a list of paymentGroupPaymentAccounts on render', () => {
        render(componentProps);

        expect(requestSpy).toHaveBeenCalled();
    });

    it('renders a list of paymentGroupPaymentAccounts', () => {
        render(componentProps);

        items.forEach(item => {
            expect(screen.getByText(item.account.accountName)).toBeTruthy();
            expect(screen.getByText(item.account.accountId)).toBeTruthy();
            expect(
                screen.getByText(item.currencyCode, { exact: false })
            ).toBeTruthy();
            expect(
                screen.getByText(
                    item.closingBalanceStatementPeriod.statementPeriodName,
                    { exact: false }
                )
            ).toBeTruthy();
            expect(
                screen.getByText(item.lastPayment, { exact: false })
            ).toBeTruthy();
            expect(
                screen.getByText(item.currentBalance, { exact: false })
            ).toBeTruthy();
            expect(
                screen.getByText(item.percentDifference, { exact: false })
            ).toBeTruthy();

            expect(
                screen.getByText(
                    smartFormatter(item.vatAmount, item.currencyCode)
                )
            ).toBeTruthy();

            /* eslint-disable jest/no-conditional-expect */
            if (parseFloat(item.paymentDifference) < 0)
                expect(
                    screen.getByText(item.paymentDifference.slice(1), {
                        exact: false,
                    })
                ).toBeTruthy();
            else
                expect(
                    screen.getByText(item.paymentDifference, { exact: false })
                ).toBeTruthy();

            if (item.note) expect(screen.getByText(item.note)).toBeTruthy();
            /* eslint-enable jest/no-conditional-expect */
        });
    });

    it('renders correct headers', () => {
        render(componentProps);
        expect(screen.getByText('Account Name')).toBeTruthy();
        expect(screen.getByText('Account ID')).toBeTruthy();
        expect(screen.getByText('Currency')).toBeTruthy();
        expect(screen.getByText('Last Payment')).toBeTruthy();
        expect(screen.getByText('Outstanding Balance')).toBeTruthy();
        expect(screen.getByText('Last Statement Period')).toBeTruthy();
        expect(
            screen.getByText('Closing Balance Statement Period')
        ).toBeTruthy();
        expect(screen.getByText('Diff')).toBeTruthy();
        expect(screen.getByText('Diff %')).toBeTruthy();
        expect(screen.getByText('Tax Withholding')).toBeTruthy();
        expect(screen.getByText('Vat Amount')).toBeTruthy();
        expect(screen.getByText('Note')).toBeTruthy();
        expect(screen.getByText('Status')).toBeTruthy();
        expect(screen.getByText('Error Message')).toBeTruthy();
        expect(screen.getByText('Actions')).toBeTruthy();
    });

    it('adds styling to positive and negative paymentDifferences', () => {
        render(componentProps);
        const paymentDiffs = items.map(item => item.paymentDifference);
        const negativeDiff = paymentDiffs.find(diff => parseFloat(diff) < 0);
        const positiveDiff = paymentDiffs.find(diff => parseFloat(diff) > 0);

        /* eslint-disable jest/no-conditional-expect */
        if (negativeDiff) {
            expect(
                screen.getByText(negativeDiff.slice(1), { exact: false })
            ).toEqual(screen.getAllByTestId('payment-diff-red')[0]);
        }
        if (positiveDiff) {
            expect(screen.getByText(positiveDiff, { exact: false })).toEqual(
                screen.getAllByTestId('payment-diff-green')[0]
            );
        }
        /* eslint-enable jest/no-conditional-expect */
    });

    it('renders links to the account details page', () => {
        render(componentProps);

        const accounts = items.map(item => item.account);

        accounts.forEach(account => {
            const { accountId, accountName } = account;
            const link = screen.getByText(accountName) as HTMLLinkElement;
            expect(link.href).toContain(`/account/${accountId}`);
        });
    });

    it('exports correct data in csv', () => {
        jest.spyOn(exportCsvUtil, 'exportCsv');
        window.URL.createObjectURL = jest.fn();

        render(componentProps);

        const exportButton = screen.getByTestId('TableExportButton');

        fireEvent.click(exportButton);

        const expectedFileName = filename;

        const csvData = [
            {
                accountId: '123',
                accountName: 'Test Account Name',
                balanceAfterTax: '265.24',
                closingBalanceStatementPeriodName: 'Jan 2023',
                currencyCode: 'USD',
                currentBalance: '400.00',
                lastPayment: '500.00',
                lastStatementPeriodId: '292',
                lastStatementPeriodName: 'April 2023',
                note: null,
                paymentDifference: '-100.00',
                paymentErrorCode: 'Error',
                paymentGroupPaymentAccountId: '5',
                paymentStatus: 'Failure',
                percentDifference: '-20.00',
                statementPeriodName: 'June 2021',
                taxWithholding: null,
                vatAmount: '55.55',
            },
            {
                accountId: '456',
                accountName: 'Other Test Account Name',
                balanceAfterTax: '65.24',
                closingBalanceStatementPeriodName: 'Feb 2023',
                currencyCode: 'CAD',
                currentBalance: '499.00',
                lastPayment: '450.00',
                lastStatementPeriodId: '292',
                lastStatementPeriodName: 'April 2023',
                note: 'Test Note',
                paymentDifference: '49.00',
                paymentErrorCode: 'Payment Rejected',
                paymentGroupPaymentAccountId: '6',
                paymentStatus: 'Canceled',
                percentDifference: '10.88',
                statementPeriodName: 'June 2021',
                taxWithholding: null,
                vatAmount: '66.66',
            },
        ];

        expect(exportCsvUtil.exportCsv).toHaveBeenCalledWith(
            csvData,
            expectedFileName
        );
    });

    it('renders a list of paymentGroupPaymentAccounts with paymentStatus and paymentErrorCode', () => {
        let accounts = paymentAccounts.abacusPaymentGroupPaymentAccounts.items;
        const count =
            paymentAccounts.abacusPaymentGroupPaymentAccounts.totalCount;
        accounts = [
            {
                ...accounts[0],
                paymentStatus: 'Canceled',
                paymentErrorCode: 'Payment Rejected',
            },
            {
                ...accounts[1],
                paymentStatus: 'Failure',
                paymentErrorCode: 'Error',
            },
        ];
        jest.spyOn(
            paymentGroupPaymentAccountQuery,
            'usePaymentGroupPaymentAccounts'
        ).mockReturnValue({
            data: {
                abacusPaymentGroupPaymentAccounts: {
                    items: accounts,
                    totalCount: count,
                },
            },
            loading: false,
            error: undefined,
        });

        render(componentProps);

        accounts.forEach(item => {
            expect(screen.getByText(item.paymentStatus)).toBeTruthy();
            expect(screen.getByText(item.paymentErrorCode)).toBeTruthy();
        });
    });
    describe('account search filters', () => {
        const defaultRequestSpyParams = {
            limit: DEFAULT_ITEMS_PER_PAGE,
            offset: 0,
            sortBy: 'account_name',
            sortOrder: 'asc',
            searchTerm: '',
        };

        it('renders', () => {
            render(componentProps);

            expect(screen.getByText('Account')).toBeDefined();
            expect(
                screen.getByPlaceholderText('Filter by Account Name or ID')
            ).toBeDefined();
            expect(screen.getByTestId('SearchFieldIcon')).toBeDefined();
        });

        it('account search filter applied', () => {
            render(componentProps);

            const input = screen.getByTestId('searchFieldInput');

            expect(input).toHaveDisplayValue('Test Search Term');
            expect(screen.getByText('Clear filters')).toBeDefined();
            expect(screen.getByTestId('CloseGlyphIcon')).toBeDefined();
            expect(requestSpy).toHaveBeenCalledWith({
                ...defaultRequestSpyParams,
                searchTerm: 'Test Search Term',
            });
        });

        it('clear account search filters', () => {
            render(componentProps);

            const clearFilters = screen.getByText('Clear filters');

            fireEvent.click(clearFilters);
            expect(requestSpy).toHaveBeenCalledWith(defaultRequestSpyParams);
        });
    });

    describe('deleting an account from a payment group', () => {
        const deleteBtnTestId = 'TableActionButton-delete';
        it('renders delete icons if payments are not sent & there is more than one account', () => {
            render(componentProps);

            const deleteButtons = screen.getAllByTestId(deleteBtnTestId);
            expect(deleteButtons).toBeDefined();
        });

        it('disables delete icons if send_payment state is running', () => {
            const newStates = {
                ...actionStates,
                send_payments: {
                    actionName: 'send_payments',
                    actionStatus: 'running',
                },
            } as unknown as PaymentAccountsTableProps['actionStates'];

            render({ ...componentProps, actionStates: newStates });

            const deleteButtons = screen.queryAllByTestId(deleteBtnTestId);
            expect(deleteButtons[0]).toHaveAttribute('disabled');
        });

        it('calls the delete mutation when delete icon is clicked', async () => {
            render(componentProps);

            const deleteButtons = screen.getAllByTestId(deleteBtnTestId);
            fireEvent.click(deleteButtons[1]);

            const confirmButton = screen.getByText('Delete');
            fireEvent.click(confirmButton);

            await waitFor(() => {
                expect(deleteSpy).toHaveBeenCalled();
            });
        });

        it('shows a message after successful deletion', async () => {
            render(componentProps);

            const deleteButtons = screen.getAllByTestId(deleteBtnTestId);
            fireEvent.click(deleteButtons[1]);

            const confirmButton = screen.getByText('Delete');
            fireEvent.click(confirmButton);

            await waitFor(() => {
                expect(
                    screen.getByText('was removed', { exact: false })
                ).toBeTruthy();
            });
        });
    });

    describe('adding a note to an account in the payment group', () => {
        it('renders with an ADD NOTE button if account does not have a note', () => {
            render(componentProps);

            expect(screen.getByText('Add Note')).toBeTruthy();
        });

        it('renders with an disabled ADD NOTE button if payment was sent', () => {
            const newStates = {
                ...actionStates,
                send_payments: {
                    actionName: 'send_payments',
                    actionStatus: 'running',
                },
            } as unknown as PaymentAccountsTableProps['actionStates'];
            render({ ...componentProps, actionStates: newStates });
            const addNotes = screen.getAllByText('Add Note');
            expect(addNotes.length).toEqual(1);
            expect(addNotes[0]).toHaveProperty('disabled', true);
        });

        it('opens a modal when ADD NOTE button is clicked', () => {
            render(componentProps);

            const addNoteButton = screen.getByText('Add Note');
            fireEvent.click(addNoteButton);
            expect(
                screen.getByTestId('payment-account-note-input')
            ).toBeTruthy();
        });

        it('calls the update mutation when a note is added', async () => {
            render(componentProps);

            const addNoteButton = screen.getByText('Add Note');
            fireEvent.click(addNoteButton);

            const saveButton = screen.getByText('Add');
            const textInput = screen.getByTestId('payment-account-note-input');

            fireEvent.change(textInput, {
                target: { value: 'non-empty string to enable the Add button' },
            });

            fireEvent.click(saveButton);

            await waitFor(() => {
                expect(updateSpy).toHaveBeenCalled();
            });
        });
    });

    describe('on funds confirmation, show approval reset warning message popup', () => {
        const deleteBtnTestId = 'TableActionButton-delete';

        it('show approval reset warning message when delete icon is clicked', () => {
            render({
                ...componentProps,
                abacusEvents: [
                    {
                        abacusEventId: '',
                        eventName: CONFIRM_FUNDS.EVENT_NAME,
                        rolledBackAt: null,
                        createdBy: '',
                        eventDate: '',
                    },
                ] as PaymentAccountsTableProps['abacusEvents'],
            });
            const deleteButtons = screen.getAllByTestId(deleteBtnTestId);
            fireEvent.click(deleteButtons[1]);

            expect(PAYMENT_APPROVAL_WARNING_MSG).toBeDefined();
        });

        it('show approval reset warning message when ADD NOTE button is clicked', () => {
            render({
                ...componentProps,
                abacusEvents: [
                    {
                        abacusEventId: '',
                        eventName: CONFIRM_FUNDS.EVENT_NAME,
                        rolledBackAt: null,
                        createdBy: '',
                        eventDate: '',
                    },
                ] as PaymentAccountsTableProps['abacusEvents'],
            });
            const addNoteButton = screen.getByText('Add Note');
            fireEvent.click(addNoteButton);

            expect(PAYMENT_APPROVAL_WARNING_MSG).toBeDefined();
        });

        it('calls the delete mutation when the Delete button of warning popup is clicked', () => {
            render({
                ...componentProps,
                abacusEvents: [
                    {
                        abacusEventId: '',
                        eventName: CONFIRM_FUNDS.EVENT_NAME,
                        rolledBackAt: null,
                        createdBy: '',
                        eventDate: '',
                    },
                ] as PaymentAccountsTableProps['abacusEvents'],
            });
            const deleteButtons = screen.getAllByTestId(deleteBtnTestId);
            fireEvent.click(deleteButtons[1]);
            expect(PAYMENT_APPROVAL_WARNING_MSG).toBeDefined();

            const confirmButton = screen.getByText('Delete');
            fireEvent.click(confirmButton);

            expect(deleteSpy).toHaveBeenCalled();
        });
    });

    it('renders delete row action and delete confirmation modal when FF is enabled', async () => {
        (suiteFrontend.useFeatureFlag as jest.Mock).mockReturnValue(true);

        render(componentProps);

        // Delete row action buttons should be present
        const deleteButtons = screen.getAllByTestId('TableActionButton-delete');
        expect(deleteButtons.length).toBeGreaterThan(0);

        // Click delete — confirmation modal should appear
        fireEvent.click(deleteButtons[1]);

        expect(
            screen.getByText(PAYMENT_ACCOUNT_DELETE_MSG, { exact: false })
        ).toBeTruthy();
    });

    it('does not render delete row action and delete confirmation modal when FF is disabled', () => {
        (suiteFrontend.useFeatureFlag as jest.Mock).mockReturnValue(false);

        render(componentProps);

        // Delete row action buttons should NOT be present
        expect(
            screen.queryAllByTestId('TableActionButton-delete')
        ).toHaveLength(0);

        // Delete confirmation modal should not exist in DOM
        expect(
            document.querySelector(
                '.PaymentGroupDetails-accounts-delete-account-warning-popup'
            )
        ).not.toBeInTheDocument();
    });
});
