import { getIFrameEl, getRootEl } from '../../test/test-utils';
import { AuthType, HostEvent } from '../../types';
import { processTrigger } from '../../utils/processTrigger';
import * as EmbedConfigService from '../embedConfig';
import {
    UIPassthroughEvent,
    UIPassthroughRequest,
    UIPassthroughArrayResponse,
    HostEventRequest,
} from './contracts';
import { HostEventClient } from './host-event-client';

jest.mock('../../utils/processTrigger');

const mockProcessTrigger = processTrigger as jest.Mock;

const createHostEventClient = () => {
    const mockIframe = document.createElement('iframe');
    const client = new HostEventClient(mockIframe);
    return { client, mockIframe };
};

describe('HostEventClient', () => {
    const mockThoughtSpotHost = 'http://localhost';
    beforeEach(() => {
        jest.spyOn(EmbedConfigService, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: mockThoughtSpotHost, authType: AuthType.None } as any);
    });

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

    describe('executeUIPassthroughApi', () => {
        it('should call processTrigger with correct parameters and return response', async () => {
            const { client, mockIframe } = createHostEventClient();

            const apiName = UIPassthroughEvent.PinAnswerToLiveboard;
            const parameters: UIPassthroughRequest<typeof apiName> = {
                newVizName: 'testViz',
            };
            const triggerResponse = Promise.resolve([
                { value: { pinboardId: 'testPinboard', tabId: 'testTab', vizId: 'testVizId' } },
            ]);

            mockProcessTrigger.mockResolvedValue(triggerResponse);

            const result = await client.triggerUIPassthroughApi(apiName, parameters);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                {
                    type: apiName,
                    parameters,
                },
                undefined,
            );
            expect(result).toEqual(await triggerResponse);
        });
    });

    describe('handleUIPassthroughForHostEvent', () => {
        it('should return the value from the first valid response', async () => {
            const { client } = createHostEventClient();
            const apiName = UIPassthroughEvent.PinAnswerToLiveboard;
            const parameters: UIPassthroughRequest<typeof apiName> = {
                newVizName: 'testViz',
            };
            const triggerResponse = Promise.resolve([
                { value: { pinboardId: 'testPinboard', tabId: 'testTab', vizId: 'testVizId' } },
            ]);
            mockProcessTrigger.mockResolvedValue(triggerResponse);

            const result = await client.handleHostEventWithParam(apiName, parameters);

            expect(result).toEqual({
                pinboardId: 'testPinboard',
                tabId: 'testTab',
                vizId: 'testVizId',
            });
        });

        it('should throw an error if no valid response is found', async () => {
            const { client } = createHostEventClient();
            const apiName = UIPassthroughEvent.PinAnswerToLiveboard;
            const parameters: UIPassthroughRequest<typeof apiName> = {
                newVizName: 'testViz',
            };
            const triggerResponse: UIPassthroughArrayResponse<typeof apiName> = [];
            mockProcessTrigger.mockResolvedValue(triggerResponse);

            await expect(client.handleHostEventWithParam(apiName, parameters))
                .rejects.toEqual({ error: 'No answer found.' });
        });

        it('should throw an error if no valid response is found for vizId', async () => {
            const { client } = createHostEventClient();
            const apiName = UIPassthroughEvent.PinAnswerToLiveboard;
            const parameters: UIPassthroughRequest<typeof apiName> = {
                newVizName: 'testViz',
                vizId: 'testVizId',
            };
            const triggerResponse: UIPassthroughArrayResponse<typeof apiName> = [];
            mockProcessTrigger.mockResolvedValue(triggerResponse);

            await expect(client.handleHostEventWithParam(apiName, parameters))
                .rejects.toEqual({ error: 'No answer found for vizId: testVizId.' });
        });

        it('should throw an error if the response contains errors', async () => {
            const { client } = createHostEventClient();
            const apiName = UIPassthroughEvent.PinAnswerToLiveboard;
            const parameters: UIPassthroughRequest<typeof apiName> = {
                newVizName: 'testViz',
            };
            const triggerResponse: UIPassthroughArrayResponse<typeof apiName> = [
                { error: 'Some error' },
            ];
            mockProcessTrigger.mockResolvedValue(triggerResponse);

            await expect(client.handleHostEventWithParam(apiName, parameters))
                .rejects.toEqual({ error: 'Some error' });
        });
    });

    describe('executeHostEvent', () => {
        const mockGetAvailablePassthroughs = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];

        it('should call handleUIPassthroughForHostEvent for Pin event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.Pin;
            const payload: HostEventRequest<typeof hostEvent> = {
                newVizName: 'testViz',
            };
            const mockResponse = {
                value: {
                    pinboardId: 'testPinboard',
                    liveboardId: 'testPinboard',
                    tabId: 'testTab',
                    vizId: 'testVizId',
                },
            };

            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([mockResponse]);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                'http://localhost',
                { parameters: payload, type: UIPassthroughEvent.PinAnswerToLiveboard },
                undefined,
            );
            expect(result).toEqual(mockResponse.value);
        });

        it('should call handleUIPassthroughForHostEvent for SaveAnswer event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.SaveAnswer;
            const payload: HostEventRequest<typeof hostEvent> = {
                name: 'Test Answer',
                description: 'Test Description',
                vizId: 'testVizId',
            };
            const mockResponse = [{
                value: {
                    saveResponse: {
                        data: {
                            Answer__save: {
                                answer: {
                                    id: 'newAnswer',
                                },
                            },
                        },
                    },
                },
                refId: 'testVizId',
            }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);
            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                {
                    parameters: payload,
                    type: 'saveAnswer',
                },
                undefined,
            );
            expect(result).toEqual({ answerId: 'newAnswer', ...mockResponse[0].value });
        });

        it('should call handleHostEventWithParam for UpdateFilters event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.UpdateFilters;
            const payload: HostEventRequest<typeof hostEvent> = {
                vizId: 'viz-123',
                filter: {
                    column: 'region',
                    oper: 'EQ',
                    values: ['North'],
                },
            } as any;
            const mockResponse = [{ value: { success: true } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                {
                    type: UIPassthroughEvent.UpdateFilters,
                    parameters: payload,
                },
                undefined,
            );
            expect(result).toEqual({ success: true });
        });

        it('should call handleHostEventWithParam for DrillDown event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.DrillDown;
            const payload: HostEventRequest<typeof hostEvent> = {
                points: { clickedPoint: 'point-1', selectedPoints: ['sel-1'] },
                autoDrillDown: true,
            } as any;
            const mockResponse = [{ value: { drillDownApplied: true } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                {
                    type: UIPassthroughEvent.Drilldown,
                    parameters: payload,
                },
                undefined,
            );
            expect(result).toEqual({ drillDownApplied: true });
        });

        it('should accept UpdateFilters with filters array', async () => {
            const { client, mockIframe } = createHostEventClient();
            const payload = {
                filters: [
                    { column: '(Sample) Retail - Apparel::city', oper: 'IN', values: ['atlanta'] },
                    { column: '(Sample) Retail - Apparel::Region', oper: 'IN', values: ['West', 'Midwest'] },
                ],
            } as any;
            const mockResponse = [{ value: { success: true } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(HostEvent.UpdateFilters, payload);

            expect(mockProcessTrigger).toHaveBeenNthCalledWith(
                2,
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.UpdateFilters, parameters: payload },
                undefined,
            );
            expect(result).toEqual({ success: true });
        });

        it('should throw when UpdateFilters payload has no valid filter', async () => {
            const { client } = createHostEventClient();
            const invalidPayload = {} as any;
            mockProcessTrigger.mockResolvedValueOnce(mockGetAvailablePassthroughs());

            await expect(client.triggerHostEvent(HostEvent.UpdateFilters, invalidPayload))
                .rejects.toThrow('UpdateFilters requires a valid filter or filters array');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should pass context to UpdateFilters event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const payload = { vizId: 'viz-1', filter: { column: 'x', oper: 'EQ', values: ['a'] } } as any;
            const context = { answerId: 'ans-1' } as any;
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: {} }]);

            await client.triggerHostEvent(HostEvent.UpdateFilters, payload, context);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.UpdateFilters, parameters: payload },
                context,
            );
        });

        it('should throw when DrillDown payload has no valid points', async () => {
            const { client } = createHostEventClient();
            const invalidPayload = {} as any;
            mockProcessTrigger.mockResolvedValueOnce(mockGetAvailablePassthroughs());

            await expect(client.triggerHostEvent(HostEvent.DrillDown, invalidPayload))
                .rejects.toThrow('DrillDown requires a valid points object');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should pass context to DrillDown event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const payload = { points: { clickedPoint: 'point-1' }, vizId: 'viz-2' } as any;
            const context = { liveboardId: 'lb-1' } as any;
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: {} }]);

            await client.triggerHostEvent(HostEvent.DrillDown, payload, context);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.Drilldown, parameters: payload },
                context,
            );
        });

        it('should skip to fallback when passthrough is not in available keys', async () => {
            const { client, mockIframe } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce([{ value: { keys: ['getFilters'] } }])
                .mockResolvedValueOnce({ session: 'legacySession' });

            const result = await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '123' });

            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
            expect(mockProcessTrigger).toHaveBeenNthCalledWith(
                2,
                mockIframe,
                HostEvent.GetAnswerSession,
                mockThoughtSpotHost,
                { vizId: '123' },
                undefined,
            );
            expect(result).toEqual({ session: 'legacySession' });
        });

        it('should call hostEventFallback for unmapped events', async () => {
            const { client } = createHostEventClient();
            const hostEvent = 'testEvent' as HostEvent;
            const payload = { data: 'testData' };
            const mockResponse = { fallbackResponse: 'data' };
            jest.spyOn(client, 'hostEventFallback').mockResolvedValue(mockResponse);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(client.hostEventFallback).toHaveBeenCalledWith(hostEvent, payload, undefined);
            expect(result).toEqual(mockResponse);
        });

        it('should route GetAnswerSession through passthrough and return data', async () => {
            const { client, mockIframe } = createHostEventClient();
            const mockResponse = [{ value: { session: 'testSession', embedAnswerData: { id: '1' } } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '123' });

            expect(mockProcessTrigger).toHaveBeenNthCalledWith(
                2,
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.GetAnswerSession, parameters: { vizId: '123' } },
                undefined,
            );
            expect(result).toEqual({ session: 'testSession', embedAnswerData: { id: '1' } });
        });

        it('should fall back to legacy host event when passthrough returns no data for GetAnswerSession', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([])
                .mockResolvedValueOnce({ session: 'fallbackSession' });

            const result = await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '123' });

            expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
            expect(mockProcessTrigger).toHaveBeenNthCalledWith(
                3,
                expect.anything(),
                HostEvent.GetAnswerSession,
                mockThoughtSpotHost,
                { vizId: '123' },
                undefined,
            );
            expect(result).toEqual({ session: 'fallbackSession' });
        });

        it('should throw real errors from passthrough without falling back', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ error: 'Permission denied' }]);

            await expect(client.triggerHostEvent(HostEvent.GetAnswerSession, {}))
                .rejects.toThrow('Permission denied');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
        });

        it('should route GetFilters through passthrough and return data', async () => {
            const { client, mockIframe } = createHostEventClient();
            const mockResponse = [{ value: { liveboardFilters: [{ id: 'f1' }], runtimeFilters: [] as any[] } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(HostEvent.GetFilters, {});

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.GetFilters, parameters: {} },
                undefined,
            );
            expect(result).toEqual({ liveboardFilters: [{ id: 'f1' }], runtimeFilters: [] });
        });

        it('should route GetTabs through passthrough and return data', async () => {
            const { client } = createHostEventClient();
            const mockResponse = [{ value: { orderedTabIds: ['t1', 't2'], numberOfTabs: 2, Tabs: [] as any[] } }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);

            const result = await client.triggerHostEvent(HostEvent.GetTabs, {});

            expect(result).toEqual({ orderedTabIds: ['t1', 't2'], numberOfTabs: 2, Tabs: [] });
        });

        it('should route GetTML through passthrough and return data', async () => {
            const { client } = createHostEventClient();
            const tmlData = { answer: { search_query: 'revenue by region' } };
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: tmlData }]);

            const result = await client.triggerHostEvent(HostEvent.GetTML, {});

            expect(result).toEqual(tmlData);
        });

        it('should route GetIframeUrl through passthrough and return data', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: { iframeUrl: 'https://ts.example.com/embed' } }]);

            const result = await client.triggerHostEvent(HostEvent.GetIframeUrl, {});

            expect(result).toEqual({ iframeUrl: 'https://ts.example.com/embed' });
        });

        it('should route GetParameters through passthrough and return data', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: { parameters: [{ name: 'p1' }] } }]);

            const result = await client.triggerHostEvent(HostEvent.GetParameters, {});

            expect(result).toEqual({ parameters: [{ name: 'p1' }] });
        });

        it('should route getExportRequestForCurrentPinboard through passthrough and return data', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([{ value: { data: { v2Content: 'exportData' }, type: 'getExportRequestForCurrentPinboard' } }]);

            const result = await client.triggerHostEvent(HostEvent.getExportRequestForCurrentPinboard, {});

            expect(result).toEqual({ data: { v2Content: 'exportData' }, type: 'getExportRequestForCurrentPinboard' });
        });

        it('should fall back to legacy for GetFilters when passthrough returns null', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(null)
                .mockResolvedValueOnce({ liveboardFilters: [], runtimeFilters: [] });

            const result = await client.triggerHostEvent(HostEvent.GetFilters, {});

            expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
            expect(result).toEqual({ liveboardFilters: [], runtimeFilters: [] });
        });

        it('should call fallback  for Pin event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.Pin;
            const payload: HostEventRequest<typeof hostEvent> = {} as any;
            const mockResponse = {
                value: {
                    pinboardId: 'testPinboard',
                    tabId: 'testTab',
                    vizId: 'testVizId',
                },
            };

            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([mockResponse]);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.Pin,
                mockThoughtSpotHost,
                {},
                undefined,
            );
            expect(result).toEqual([mockResponse]);
        });

        it('should call fallback for SaveAnswer event', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.SaveAnswer;
            const payload: HostEventRequest<typeof hostEvent> = {} as any;
            const mockResponse = {
                value: {
                    pinboardId: 'testPinboard',
                    tabId: 'testTab',
                    vizId: 'testVizId',
                },
            };

            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce([mockResponse]);

            const result = await client.triggerHostEvent(hostEvent, payload);

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.Save,
                mockThoughtSpotHost,
                {},
                undefined,
            );
            expect(result).toEqual([mockResponse]);
        });

        it('Pin response support pinboardId as well', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.Pin;
            const payload: HostEventRequest<typeof hostEvent> = {
                newVizDescription: 'Test Description',
                vizId: 'testVizId',
                newVizName: 'Test Answer',
                newLiveboardName: 'testLiveboard',
            } as any;
            const mockResponse = [{
                value: {
                    pinboardId: 'testLiveboard',
                    liveboardId: 'testPinboard',
                    tabId: 'testTab',
                    vizId: 'testVizId',
                },
                refId: 'testVizId',
            }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);
            const result = await client.triggerHostEvent(hostEvent, payload);
            expect(result.liveboardId).toBe('testLiveboard');
        });

        it('should request liveboardId as well', async () => {
            const { client, mockIframe } = createHostEventClient();
            const hostEvent = HostEvent.Pin;
            const payload: HostEventRequest<typeof hostEvent> = {
                liveboardId: 'test',
                newVizName: 'Test Answer',
                newPinboardName: 'testLiveboard1',
                newLiveboardName: 'testLiveboard',
            } as any;
            const mockResponse = [{
                value: {
                    pinboardId: 'testLiveboard',
                    tabId: 'testTab',
                    vizId: 'testVizId',
                },
                refId: 'testVizId',
            }];
            mockProcessTrigger
                .mockResolvedValueOnce(mockGetAvailablePassthroughs())
                .mockResolvedValueOnce(mockResponse);
            const result = await client.triggerHostEvent(hostEvent, payload);
            expect(result.liveboardId).toBe('testLiveboard');
            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                {
                    parameters: { ...payload, pinboardId: 'test', newPinboardName: 'testLiveboard' },
                    type: 'addVizToPinboard',
                },
                undefined,
            );
            expect(result).toEqual({
                pinboardId: 'testLiveboard',
                tabId: 'testTab',
                vizId: 'testVizId',
                liveboardId: 'testLiveboard',
            });
        });
    });

    describe('getDataWithPassthroughFallback', () => {
        const callMethod = (client: HostEventClient, ...args: any[]) => (client as any).getDataWithPassthroughFallback(...args);

        it('should return unwrapped value when passthrough succeeds', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue([{ value: { session: 's1', embedAnswerData: { id: 'a1' } } }]);

            const result = await callMethod(
                client, UIPassthroughEvent.GetAnswerSession, HostEvent.GetAnswerSession, { vizId: '1' },
            );

            expect(result).toEqual({ session: 's1', embedAnswerData: { id: 'a1' } });
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should fall back to legacy when passthrough returns empty array', async () => {
            const { client } = createHostEventClient();
            const legacyResponse = { session: 'legacy' };
            mockProcessTrigger
                .mockResolvedValueOnce([])
                .mockResolvedValueOnce(legacyResponse);

            const result = await callMethod(
                client, UIPassthroughEvent.GetAnswerSession, HostEvent.GetAnswerSession, { vizId: '1' },
            );

            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
            expect(result).toEqual(legacyResponse);
        });

        it('should fall back to legacy when passthrough returns null', async () => {
            const { client } = createHostEventClient();
            const legacyResponse = { parameters: [] as any[] };
            mockProcessTrigger
                .mockResolvedValueOnce(null)
                .mockResolvedValueOnce(legacyResponse);

            const result = await callMethod(
                client, UIPassthroughEvent.GetParameters, HostEvent.GetParameters, {},
            );

            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
            expect(result).toEqual(legacyResponse);
        });

        it('should fall back to legacy when passthrough returns undefined', async () => {
            const { client } = createHostEventClient();
            const legacyResponse = { iframeUrl: 'https://ts.example.com' };
            mockProcessTrigger
                .mockResolvedValueOnce(undefined)
                .mockResolvedValueOnce(legacyResponse);

            const result = await callMethod(
                client, UIPassthroughEvent.GetIframeUrl, HostEvent.GetIframeUrl, {},
            );

            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
            expect(result).toEqual(legacyResponse);
        });

        it('should fall back when response array has no matching entries', async () => {
            const { client } = createHostEventClient();
            const legacyResponse = { v2Content: 'data' };
            mockProcessTrigger
                .mockResolvedValueOnce([{ refId: 'r1' }])
                .mockResolvedValueOnce(legacyResponse);

            const result = await callMethod(
                client, UIPassthroughEvent.GetExportRequestForCurrentPinboard,
                HostEvent.getExportRequestForCurrentPinboard, {},
            );

            expect(mockProcessTrigger).toHaveBeenCalledTimes(2);
            expect(result).toEqual(legacyResponse);
        });

        it('should throw when response has error field', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue([{ error: 'Permission denied' }]);

            await expect(callMethod(
                client, UIPassthroughEvent.GetFilters, HostEvent.GetFilters, {},
            )).rejects.toThrow('Permission denied');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should throw when response value contains errors field', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue([{ value: { errors: 'Invalid vizId' } }]);

            await expect(callMethod(
                client, UIPassthroughEvent.GetTML, HostEvent.GetTML, { vizId: 'bad' },
            )).rejects.toThrow('Invalid vizId');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should throw when response value contains error field', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue([{ value: { error: 'Not found' } }]);

            await expect(callMethod(
                client, UIPassthroughEvent.GetTabs, HostEvent.GetTabs, {},
            )).rejects.toThrow('Not found');
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should stringify object errors instead of producing [object Object]', async () => {
            const { client } = createHostEventClient();
            const errorObj = { code: 403, reason: 'Forbidden' };
            mockProcessTrigger.mockResolvedValue([{ error: errorObj }]);

            await expect(callMethod(
                client, UIPassthroughEvent.GetFilters, HostEvent.GetFilters, {},
            )).rejects.toThrow(JSON.stringify(errorObj));
            expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
        });

        it('should default payload to empty object when null', async () => {
            const { client, mockIframe } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue([{ value: { iframeUrl: 'https://ts.example.com' } }]);

            await callMethod(
                client, UIPassthroughEvent.GetIframeUrl, HostEvent.GetIframeUrl, null,
            );

            expect(mockProcessTrigger).toHaveBeenCalledWith(
                mockIframe,
                HostEvent.UIPassthrough,
                mockThoughtSpotHost,
                { type: UIPassthroughEvent.GetIframeUrl, parameters: {} },
                undefined,
            );
        });
    });

    describe('UI passthrough available keys tests', () => {
        const mockKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];

        it('triggerHostEvent Pin returns passthrough response', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockKeys())
                .mockResolvedValueOnce([{ value: { pinboardId: 'lb1', tabId: 't1', vizId: 'v1' } }]);

            const result = await client.triggerHostEvent(HostEvent.Pin, { newVizName: 'Viz' });

            expect(result).toMatchObject({ pinboardId: 'lb1', liveboardId: 'lb1' });
        });

        it('triggerHostEvent GetAnswerSession returns session', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger
                .mockResolvedValueOnce(mockKeys())
                .mockResolvedValueOnce([{ value: { session: 's1' } }]);

            const result = await client.triggerHostEvent(HostEvent.GetAnswerSession, {});

            expect(result).toEqual({ session: 's1' });
        });

        it('triggerHostEvent unmapped event uses fallback', async () => {
            const { client } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue({ data: 'legacy' });

            const result = await client.triggerHostEvent('unknownEvent' as HostEvent, {});

            expect(mockProcessTrigger).toHaveBeenCalledWith(expect.anything(), 'unknownEvent', mockThoughtSpotHost, {}, undefined);
            expect(result).toEqual({ data: 'legacy' });
        });

        it('hostEventFallback delegates to processTrigger', async () => {
            const { client, mockIframe } = createHostEventClient();
            mockProcessTrigger.mockResolvedValue({ ok: true });

            const result = await client.hostEventFallback(HostEvent.Save, { x: 1 });

            expect(mockProcessTrigger).toHaveBeenCalledWith(mockIframe, HostEvent.Save, mockThoughtSpotHost, { x: 1 }, undefined);
            expect(result).toEqual({ ok: true });
        });
    });
});
