Загрузка данных


import { act, render } from '@testing-library/react';

import { MoexChart, Timeframes } from 'moex-chart';
import React from 'react';

import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';

import { useMoexChart } from '../components/MoexChart/hooks';

import type { ChartIndicativeData } from '@widgets/Chart/types';

jest.mock('@modules/widgetProperties');

jest.mock('../components/MoexChart/dataSourceProvide', () => ({
  DataSourceProvider: jest.fn(),
}));

jest.mock('moex-chart', () => ({
  MoexChart: jest.fn(),
  Timeframes: {
    '10s': '10s',
    '1m': '1m',
    '5m': '5m',
  },
  DateFormat: {
    DD_MM_YYYY_HH_mm_ss: 'DD_MM_YYYY_HH_mm_ss',
  },
  IndicatorsIds: {
    Volume: 'Volume',
  },
  Locale: {
    rus: 'ru-RU',
    eng: 'en-US',
  },
}));

jest.mock('@api/index', () => ({
  updateMenuLocked: jest.fn(),
  widgetsController: {
    delete: jest.fn(),
  },
  widgetPropertiesController: {
    update: jest.fn(),
  },
  workspaceController: {
    update: jest.fn(),
  },
}));

jest.mock('@api/controllers/workspace', () => ({
  workspaceController: {
    update: jest.fn(),
  },
}));

const mockedDataSourceProvide = jest.requireMock('../components/MoexChart/dataSourceProvide') as {
  DataSourceProvider: jest.Mock;
};

type TimeframeValue = (typeof Timeframes)[keyof typeof Timeframes];

interface MockCompareSeries {
  name: string;
  seriesOptions?: {
    priceScaleId?: string;
    color?: string;
  };
}

interface MockIndicatorConfig {
  symbol?: string;
  symbolTicker?: string;
  label?: string;
  newPane?: boolean;
  series: MockCompareSeries[];
}

interface MockIndicatorSnapshot {
  id?: string;
  indicatorType?: string;
  dataSource?: unknown;
  config?: MockIndicatorConfig;
}

interface MockPaneSnapshot {
  indicators: MockIndicatorSnapshot[];
}

interface MockChartSnapshot {
  charts: {
    symbol: string;
    symbolName?: string;
    symbolTicker?: string;
    timeframe: TimeframeValue;
    chartSeriesType: string;
    panes: MockPaneSnapshot[];
  }[];
}

interface MockMoexChartConfig {
  container: HTMLElement;
  snapshot: MockChartSnapshot;
  chartCollectionPreset: {
    openCompareModal: () => void;
    openSymbolSearchModal: () => void;
    getDataSource: jest.Mock;
    startRealtime: (
      getSymbols: () => string[],
      getTimeframe: () => TimeframeValue,
      update: jest.Mock,
    ) => (() => void) | undefined;
  };
}

interface MockMoexChartState {
  timeframe: TimeframeValue;
  initialInterval?: string;
  savedData?: string;
}

interface MockPropertiesState {
  moexChartState?: MockMoexChartState;
}

type UpdatePropertiesCallback = (state: MockPropertiesState) => void;

type TimeframeChangeCallback = (timeframe: TimeframeValue) => void;

interface TestComponentProps {
  symbol?: string;
  symbolName?: string;
  symbolTicker?: string;
  indicativeData?: ChartIndicativeData;
}

describe('useMoexChart', () => {
  const mockUseSelectProperties = useSelectProperties as jest.Mock;
  const mockUseChangeProperties = useChangeProperties as jest.Mock;
  const mockMoexChart = MoexChart as jest.Mock;
  const mockDataSourceProvider = mockedDataSourceProvide.DataSourceProvider;

  const mockUpdateProperties = jest.fn();

  const mockDestroy = jest.fn();
  const mockGetSnapshot = jest.fn();
  const mockSetSnapshot = jest.fn();
  const mockSetSymbol = jest.fn();
  const mockGetCompareManager = jest.fn();
  const mockSetSettings = jest.fn();

  const mockDataSource = jest.fn();
  const mockStartRealtime = jest.fn();
  const mockGetDataSource = jest.fn();
  const mockRealtimeUnsubscribe = jest.fn();

  const mockCompareManager = {
    id: 'compare-manager',
  };

  const mockSnapshot: MockChartSnapshot = {
    charts: [
      {
        symbol: 'OLD:SYMBOL',
        symbolName: 'Old instrument',
        symbolTicker: 'OLD',
        timeframe: Timeframes['5m'],
        chartSeriesType: 'Candlestick',
        panes: [
          {
            indicators: [],
          },
        ],
      },
    ],
  };

  let hookResult: ReturnType<typeof useMoexChart> | null = null;
  let lastMoexChartConfig: MockMoexChartConfig | null = null;
  let mockMoexChartState: MockMoexChartState | undefined;

  const TestComponent = ({
    symbol = 'MOEX:SBER',
    symbolName = 'Сбербанк',
    symbolTicker = 'SBER',
    indicativeData,
  }: TestComponentProps): React.ReactElement => {
    hookResult = useMoexChart({
      symbol,
      symbolName,
      symbolTicker,
      indicativeData,
    });

    return <div ref={hookResult.containerRef} />;
  };

  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();

    hookResult = null;
    lastMoexChartConfig = null;

    mockMoexChartState = {
      timeframe: Timeframes['1m'],
      savedData: undefined,
    };

    mockUseSelectProperties.mockImplementation((selector: (state: MockPropertiesState) => unknown) =>
      selector({
        moexChartState: mockMoexChartState,
      }),
    );

    mockUseChangeProperties.mockReturnValue({
      updateProperties: mockUpdateProperties,
    });

    mockGetDataSource.mockImplementation(
      (_indicativeData?: ChartIndicativeData, callback?: (timeframe: TimeframeValue) => void) =>
        (timeframe: TimeframeValue) => {
          callback?.(timeframe);

          return mockDataSource();
        },
    );

    mockStartRealtime.mockReturnValue(mockRealtimeUnsubscribe);

    mockDataSourceProvider.mockImplementation(() => ({
      getDataSource: mockGetDataSource,
      startRealtime: mockStartRealtime,
    }));

    mockGetSnapshot.mockReturnValue(mockSnapshot);
    mockGetCompareManager.mockReturnValue(mockCompareManager);

    mockMoexChart.mockImplementation((config: MockMoexChartConfig) => {
      lastMoexChartConfig = config;

      return {
        destroy: mockDestroy,
        getSnapshot: mockGetSnapshot,
        setSnapshot: mockSetSnapshot,
        setSymbol: mockSetSymbol,
        setSettings: mockSetSettings,
        getCompareManager: mockGetCompareManager,
      };
    });
  });

  afterEach(() => {
    jest.clearAllTimers();
    jest.useRealTimers();
  });

  it('should create moex chart with current symbol, name, ticker and timeframe', () => {
    // Arrange & Act
    render(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName="Сбербанк"
        symbolTicker="SBER"
      />,
    );

    // Assert
    expect(mockMoexChart).toHaveBeenCalledTimes(1);
    expect(mockDataSourceProvider).toHaveBeenCalledTimes(1);
    expect(lastMoexChartConfig?.container).toBeInstanceOf(HTMLDivElement);
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:SBER');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolName).toBe('Сбербанк');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolTicker).toBe('SBER');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.timeframe).toBe(Timeframes['1m']);
    expect(hookResult?.compareManagerRef.current).toBe(mockCompareManager);
    expect(hookResult?.hasSavedSnapshot).toBe(false);
  });

  it('should create chart with saved snapshot and current instrument data', () => {
    // Arrange
    mockMoexChartState = {
      timeframe: Timeframes['5m'],
      savedData: JSON.stringify(mockSnapshot),
    };

    // Act
    render(
      <TestComponent
        symbol="MOEX:GAZP"
        symbolName="Газпром"
        symbolTicker="GAZP"
      />,
    );

    // Assert
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:GAZP');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolName).toBe('Газпром');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolTicker).toBe('GAZP');
    expect(lastMoexChartConfig?.snapshot.charts[0]?.timeframe).toBe(Timeframes['5m']);
    expect(hookResult?.hasSavedSnapshot).toBe(true);
  });

  it('should save chart snapshot to widget properties', () => {
    // Arrange
    render(<TestComponent />);

    // Act
    act(() => {
      hookResult?.saveSnapshot();
    });

    // Assert
    expect(mockGetSnapshot).toHaveBeenCalledTimes(1);
    expect(mockUpdateProperties).toHaveBeenCalledTimes(1);

    const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;

    const mockState: MockPropertiesState = {
      moexChartState: {
        timeframe: Timeframes['1m'],
      },
    };

    updateCallback(mockState);

    expect(mockState.moexChartState).toEqual({
      timeframe: Timeframes['1m'],
      savedData: JSON.stringify(mockSnapshot),
    });
  });

  it('should serialize compare symbol, display name and ticker in snapshot', () => {
    // Arrange
    const snapshotWithIndicators: MockChartSnapshot = {
      charts: [
        {
          symbol: 'MOEX:SBER',
          symbolName: 'Сбербанк',
          symbolTicker: 'SBER',
          timeframe: Timeframes['1m'],
          chartSeriesType: 'Candlestick',
          panes: [
            {
              indicators: [
                {
                  id: 'compare-gazp',
                  indicatorType: undefined,
                  dataSource: {
                    subscription: {},
                  },
                  config: {
                    symbol: 'MOEX:GAZP',
                    symbolTicker: 'GAZP',
                    label: 'Газпром',
                    newPane: false,
                    series: [
                      {
                        name: 'Line',
                        seriesOptions: {
                          priceScaleId: 'left',
                          color: '#FFFFFF',
                        },
                      },
                    ],
                  },
                },
                {
                  id: 'rsi',
                  indicatorType: 'RSI',
                  dataSource: {
                    subscription: {},
                  },
                  config: {
                    label: 'RSI',
                    newPane: true,
                    series: [
                      {
                        name: 'Line',
                        seriesOptions: {
                          priceScaleId: 'right',
                        },
                      },
                    ],
                  },
                },
              ],
            },
          ],
        },
      ],
    };

    mockGetSnapshot.mockReturnValue(snapshotWithIndicators);

    render(<TestComponent />);

    // Act
    act(() => {
      hookResult?.saveSnapshot();
    });

    const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;

    const mockState: MockPropertiesState = {
      moexChartState: {
        timeframe: Timeframes['1m'],
      },
    };

    updateCallback(mockState);

    const savedSnapshot = JSON.parse(mockState.moexChartState?.savedData ?? '{}') as MockChartSnapshot;
    const [compareIndicator, regularIndicator] = savedSnapshot.charts[0]?.panes[0]?.indicators ?? [];

    // Assert
    expect(compareIndicator).toEqual({
      id: 'compare-gazp',
      config: {
        symbol: 'MOEX:GAZP',
        symbolTicker: 'GAZP',
        label: 'Газпром',
        newPane: false,
        series: [
          {
            name: 'Line',
            seriesOptions: {
              priceScaleId: 'left',
            },
          },
        ],
      },
    });

    expect(regularIndicator).toEqual({
      id: 'rsi',
      indicatorType: 'RSI',
    });
  });

  it('should not save snapshot when chart does not return snapshot', () => {
    // Arrange
    mockGetSnapshot.mockReturnValue(undefined);

    render(<TestComponent />);

    // Act
    act(() => {
      hookResult?.saveSnapshot();
    });

    // Assert
    expect(mockGetSnapshot).toHaveBeenCalledTimes(1);
    expect(mockUpdateProperties).not.toHaveBeenCalled();
  });

  it('should apply saved snapshot with current instrument data and timeframe', () => {
    // Arrange
    mockMoexChartState = {
      timeframe: Timeframes['5m'],
      savedData: JSON.stringify(mockSnapshot),
    };

    render(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName="Сбербанк"
        symbolTicker="SBER"
      />,
    );

    // Act
    act(() => {
      hookResult?.applySnapshot();
    });

    // Assert
    expect(mockSetSnapshot).toHaveBeenCalledWith({
      ...mockSnapshot,
      charts: [
        {
          ...mockSnapshot.charts[0],
          symbol: 'MOEX:SBER',
          symbolName: 'Сбербанк',
          symbolTicker: 'SBER',
          timeframe: Timeframes['5m'],
        },
      ],
    });

    expect(hookResult?.compareManagerRef.current).toBe(mockCompareManager);
  });

  it('should update compare modal state', () => {
    // Arrange
    render(<TestComponent />);

    // Act
    act(() => {
      hookResult?.setIsCompareOpen(true);
    });

    // Assert
    expect(hookResult?.isCompareOpen).toBe(true);
  });

  it('should open compare modal from chart preset callback', () => {
    // Arrange
    render(<TestComponent />);

    // Act
    act(() => {
      lastMoexChartConfig?.chartCollectionPreset.openCompareModal();
    });

    // Assert
    expect(hookResult?.isCompareOpen).toBe(true);
  });

  it('should update symbol search modal state', () => {
    // Arrange
    render(<TestComponent />);

    // Act
    act(() => {
      hookResult?.setIsSymbolSearchOpen(true);
    });

    // Assert
    expect(hookResult?.isSymbolSearchOpen).toBe(true);
  });

  it('should open symbol search modal from chart preset callback', () => {
    // Arrange
    render(<TestComponent />);

    // Act
    act(() => {
      lastMoexChartConfig?.chartCollectionPreset.openSymbolSearchModal();
    });

    // Assert
    expect(hookResult?.isSymbolSearchOpen).toBe(true);
  });

  it('should update chart when external instrument changes', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:GAZP"
        symbolName="Газпром"
        symbolTicker="GAZP"
      />,
    );

    // Assert
    expect(mockSetSymbol).toHaveBeenCalledTimes(1);
    expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:GAZP', 'Газпром', 'GAZP');
  });

  it('should update chart when only external symbol name changes', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName="Сбербанк ПАО"
        symbolTicker="SBER"
      />,
    );

    // Assert
    expect(mockSetSymbol).toHaveBeenCalledTimes(1);
    expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк ПАО', 'SBER');
  });

  it('should update chart when only external symbol ticker changes', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName="Сбербанк"
        symbolTicker="SBERP"
      />,
    );

    // Assert
    expect(mockSetSymbol).toHaveBeenCalledTimes(1);
    expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк', 'SBERP');
  });

  it('should use symbol as display name when symbol name is empty', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName=""
        symbolTicker="SBER"
      />,
    );

    // Assert
    expect(mockSetSymbol).toHaveBeenCalledTimes(1);
    expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'MOEX:SBER', 'SBER');
  });

  it('should use symbol as ticker when symbol ticker is empty', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:SBER"
        symbolName="Сбербанк"
        symbolTicker=""
      />,
    );

    // Assert
    expect(mockSetSymbol).toHaveBeenCalledTimes(1);
    expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк', 'MOEX:SBER');
  });

  it('should not update chart when symbol, name and ticker are unchanged', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(<TestComponent />);

    // Assert
    expect(mockSetSymbol).not.toHaveBeenCalled();
  });

  it('should not update chart when external symbol is empty', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol=""
        symbolName="Пустой инструмент"
        symbolTicker="EMPTY"
      />,
    );

    // Assert
    expect(mockSetSymbol).not.toHaveBeenCalled();
  });

  it('should not recreate chart when external instrument changes', () => {
    // Arrange
    const { rerender } = render(<TestComponent />);

    // Act
    rerender(
      <TestComponent
        symbol="MOEX:GAZP"
        symbolName="Газпром"
        symbolTicker="GAZP"
      />,
    );

    // Assert
    expect(mockMoexChart).toHaveBeenCalledTimes(1);
  });

  it('should apply saved snapshot with externally selected instrument', () => {
    // Arrange
    mockMoexChartState = {
      timeframe: Timeframes['5m'],
      savedData: JSON.stringify(mockSnapshot),
    };

    const { rerender } = render(<TestComponent />);

    rerender(
      <TestComponent
        symbol="MOEX:GAZP"
        symbolName="Газпром"
        symbolTicker="GAZP"
      />,
    );

    // Act
    act(() => {
      hookResult?.applySnapshot();
    });

    // Assert
    expect(mockSetSnapshot).toHaveBeenCalledWith({
      ...mockSnapshot,
      charts: [
        {
          ...mockSnapshot.charts[0],
          symbol: 'MOEX:GAZP',
          symbolName: 'Газпром',
          symbolTicker: 'GAZP',
          timeframe: Timeframes['5m'],
        },
      ],
    });
  });

  it('should initialize indicative instrument with id, name and ticker', () => {
    // Arrange
    const indicativeData: ChartIndicativeData = {
      id: 1,
      title: 'Indicative instrument',
      secId: 'INAV',
      instrumentName: 'Индикатив',
      settlement: 'Расчётный',
      firmName: 'Тестовая фирма',
      key: '2xOFZ:INAV',
    };

    // Act
    render(
      <TestComponent
        symbol="2xOFZ:INAV"
        symbolName="Индикатив Расчётный"
        symbolTicker="INAV"
        indicativeData={indicativeData}
      />,
    );

    // Assert
    expect(mockGetDataSource).toHaveBeenCalledWith(indicativeData, expect.any(Function));
    expect(lastMoexChartConfig?.snapshot.charts[0]).toEqual(
      expect.objectContaining({
        symbol: '2xOFZ:INAV',
        symbolName: 'Индикатив Расчётный',
        symbolTicker: 'INAV',
      }),
    );
  });

  it('should pass realtime params to data source provider', () => {
    // Arrange
    render(<TestComponent />);

    const getSymbols = jest.fn(() => ['MOEX:SBER']);
    const getTimeframe = jest.fn(() => Timeframes['1m']);
    const update = jest.fn();

    // Act
    const unsubscribe = lastMoexChartConfig?.chartCollectionPreset.startRealtime(getSymbols, getTimeframe, update);

    // Assert
    expect(mockStartRealtime).toHaveBeenCalledWith({
      getSymbols,
      getTimeframe,
      update,
    });
    expect(unsubscribe).toBe(mockRealtimeUnsubscribe);
  });

  it('should update timeframe from data source callback', () => {
    // Arrange
    render(<TestComponent />);

    const timeframeChangeCallback = mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;

    // Act
    act(() => {
      timeframeChangeCallback(Timeframes['5m']);
    });

    // Assert
    expect(mockUpdateProperties).toHaveBeenCalledTimes(1);

    const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;

    const mockState: MockPropertiesState = {
      moexChartState: {
        timeframe: Timeframes['1m'],
      },
    };

    updateCallback(mockState);

    expect(mockState.moexChartState?.timeframe).toBe(Timeframes['5m']);
  });

  it('should not update timeframe when it is the same as current timeframe', () => {
    // Arrange
    render(<TestComponent />);

    const timeframeChangeCallback = mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;

    // Act
    act(() => {
      timeframeChangeCallback(Timeframes['1m']);
    });

    // Assert
    expect(mockUpdateProperties).not.toHaveBeenCalled();
  });

  it('should destroy chart on unmount', () => {
    // Arrange
    const { unmount } = render(<TestComponent />);

    // Act
    unmount();

    // Assert
    expect(mockDestroy).toHaveBeenCalledTimes(1);
  });
});


import { renderHook } from '@testing-library/react';

import { useAppSelect } from '@hooks/useAppSelector';
import { useContracts } from '@modules/contracts';
import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';

import { useChartPublicContext } from '../hooks/useChartPublicContext';

import type { Contract } from '@modules/contracts';

// Mock the dependencies
jest.mock('@hooks/useAppSelector');
jest.mock('@modules/contracts');
jest.mock('@utils/filterByUniqIssKey');

jest.mock('@api/index', () => ({
  updateMenuLocked: jest.fn(),
  widgetsController: {
    delete: jest.fn(),
  },
  widgetPropertiesController: {
    update: jest.fn(),
  },
  workspaceController: {
    update: jest.fn(),
  },
}));

jest.mock('@api/controllers/workspace', () => ({
  workspaceController: {
    update: jest.fn(),
  },
}));

describe('useChartPublicContext', () => {
  const mockUseAppSelect = useAppSelect as jest.Mock;
  const mockUseContracts = useContracts as jest.Mock;
  const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;

  const mockContracts = [
    {
      issKey: DEFAULT_SYMBOL,
      displayName: 'Инструмент по умолчанию',
      symbol: 'DEFAULT',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST1',
      displayName: 'Test Instrument 1',
      symbol: 'TEST1',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST2',
      displayName: 'Test Instrument 2',
      symbol: 'TEST2',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST3',
      displayName: 'Test Instrument 3',
      symbol: 'TEST3',
      // ... other contract properties
    },
  ] as Contract[];

  const mockWidget = {
    id: 1,
    name: 'Test Widget',
    type: 'graphic',
    master: 123,
    externalProperties: [
      {
        key: 'instrument',
        value: 'MOEX:TEST1',
      },
    ],
    // ... other widget properties
  };

  const mockPublicContext = {
    instrument: 'MOEX:TEST1',
  };

  beforeEach(() => {
    jest.clearAllMocks();

    // Mock the useAppSelect to return our test data
    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }

      if (selector.toString().includes('widgets')) {
        return mockWidget;
      }

      return {};
    });

    // Mock useContracts to return our test contracts
    mockUseContracts.mockReturnValue({
      contracts: mockContracts,
    });

    // Mock filterByUniqIssKey to return the same contracts
    mockFilterByUniqIssKey.mockImplementation((contracts: Contract[]) => contracts);
  });

  it('should return the correct issKey when a matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
  });

  it('should pass instrument id and display name when a matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
    expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2', 'Test Instrument 2', 'TEST2');
  });

  it('should return undefined when no matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
    expect(mockSetCurrInstrument).not.toHaveBeenCalled();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should set current instrument to DEFAULT_SYMBOL with display name when no field value and widget has master', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
    expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL, 'Инструмент по умолчанию', 'DEFAULT');
  });

  it('should handle case when widget is not found in widgets array', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Mock useAppSelect to return null for widget
    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }

      if (selector.toString().includes('widgets')) {
        return null; // Widget not found
      }

      return {};
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 999, // Non-existent widget ID
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
    expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST1', 'Test Instrument 1', 'TEST1');
  });

  it('should handle case when contracts array is empty', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Mock useContracts to return empty contracts
    mockUseContracts.mockReturnValue({
      contracts: [],
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
    expect(mockSetCurrInstrument).not.toHaveBeenCalled();
  });
});


import { act, renderHook } from '@testing-library/react';

import { useDispatch } from 'react-redux';

import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
import { getState } from '@store/store';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import useChartComponentFacade from '../hooks/useChartComponentFacade';
import { useChartPublicContext } from '../hooks/useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';

jest.mock('@store/store', () => ({
  getState: jest.fn(),
}));
jest.mock('@store/slices/widgets', () => ({
  addContentPropsToWidget: jest.fn(),
  unbindWidgets: jest.fn(),
}));
jest.mock('react-redux', () => ({
  useDispatch: jest.fn(),
}));
jest.mock('@core/comm', () => ({
  communicator: {
    listen: jest.fn(),
  },
}));
jest.mock('@hooks/useAppSelector', () => ({
  useAppSelect: jest.fn(),
}));
jest.mock('@utils/hooks/useWidgetsBind', () => ({
  useWidgetsBind: jest.fn(),
}));
jest.mock('../hooks/useChartPublicContext', () => ({
  useChartPublicContext: jest.fn(),
}));

interface PublicContextMockParams {
  setCurrInstrument: (instrumentId: string, instrumentName?: string, instrumentTicker?: string) => void;
}

describe('useChartComponentFacade', () => {
  const mockUseDispatch = useDispatch as jest.Mock;
  const mockUseAppSelect = useAppSelect as jest.Mock;
  const mockAddContentPropsToWidget = addContentPropsToWidget as unknown as jest.Mock;
  const mockUnbindWidgets = unbindWidgets as unknown as jest.Mock;
  const mockGetState = getState as jest.Mock;
  const mockUseWidgetsBind = useWidgetsBind as jest.Mock;
  const mockUseChartPublicContext = useChartPublicContext as jest.Mock;
  const mockCommunicatorListen = communicator.listen as jest.Mock;

  const mockDispatch = jest.fn();
  const mockTriggerRelatedWidgetsToUpdate = jest.fn();
  const mockGetMasterInstrumentFromPublicContext = jest.fn();

  const mockUnsubscribeCorpActions = jest.fn();
  const mockUnsubscribeHighlighter = jest.fn();

  const widgetProperties = {
    chartState: {
      savedInstrument: 'MOEX:SBER',
      savedInstrumentName: 'Сбербанк',
      savedInstrumentTicker: 'SBER',
      interval: '1m',
    },
    indicativeData: {
      id: 1,
      key: 'INDICATIVE',
      secId: 'INAV',
      instrumentName: 'Индикатив',
      settlement: 'Расчётный',
      firmName: 'Тестовая фирма',
    },
  } as WidgetProperties;

  const renderFacade = () =>
    renderHook(() =>
      useChartComponentFacade({
        widgetId: 42,
      } as ChartContainerProps),
    );

  beforeEach(() => {
    jest.clearAllMocks();

    mockUseDispatch.mockReturnValue(mockDispatch);
    mockUseAppSelect.mockReturnValue(widgetProperties);

    mockGetState.mockReturnValue({
      widgets: {
        widgets: [
          {
            id: 42,
            widgetContentProps: widgetProperties,
          },
        ],
      },
    });

    mockUseWidgetsBind.mockReturnValue({
      triggerRelatedWidgetsToUpdate: mockTriggerRelatedWidgetsToUpdate,
      getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
    });

    mockUseChartPublicContext.mockReturnValue({
      issKey: undefined,
    });

    mockAddContentPropsToWidget.mockImplementation((payload) => ({
      type: 'widgets/addContentPropsToWidget',
      payload,
    }));

    mockUnbindWidgets.mockImplementation((payload) => ({
      type: 'widgets/unbindWidgets',
      payload,
    }));

    mockCommunicatorListen.mockImplementation(({ messageType }, listener) => {
      if (messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT) {
        return mockUnsubscribeCorpActions;
      }

      if (messageType === HIGHLIGHT_WIDGET_EVENT) {
        return mockUnsubscribeHighlighter;
      }

      return jest.fn();
    });
  });

  it('should initialize symbol, display name and ticker from widget properties', () => {
    // Arrange & Act
    const { result } = renderFacade();

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:SBER');
    expect(result.current.currentInstrumentName).toBe('Сбербанк');
    expect(result.current.currentInstrumentTicker).toBe('SBER');
  });

  it('should update and save instrument selected from modal', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([
        {
          issKey: 'MOEX:GAZP',
          displayName: 'Газпром',
          symbol: 'GAZP',
        },
      ]);
    });

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:GAZP');
    expect(result.current.currentInstrumentName).toBe('Газпром');
    expect(result.current.currentInstrumentTicker).toBe('GAZP');

    expect(mockUnbindWidgets).toHaveBeenCalledWith({
      widgetId: 42,
    });

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
      id: 42,
      withoutSend: true,
      widgetContentProps: {
        chartState: {
          ...widgetProperties.chartState,
          savedInstrument: 'MOEX:GAZP',
          savedInstrumentName: 'Газпром',
          savedInstrumentTicker: 'GAZP',
        },
        indicativeData: undefined,
      },
    });

    expect(mockTriggerRelatedWidgetsToUpdate).toHaveBeenCalledWith('MOEX:GAZP');
  });

  it('should update name and ticker without unbinding when symbol is unchanged', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([
        {
          issKey: 'MOEX:SBER',
          displayName: 'Сбербанк ПАО',
          symbol: 'SBERP',
        },
      ]);
    });

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:SBER');
    expect(result.current.currentInstrumentName).toBe('Сбербанк ПАО');
    expect(result.current.currentInstrumentTicker).toBe('SBERP');

    expect(mockUnbindWidgets).not.toHaveBeenCalled();
    expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
      id: 42,
      withoutSend: true,
      widgetContentProps: {
        chartState: {
          ...widgetProperties.chartState,
          savedInstrument: 'MOEX:SBER',
          savedInstrumentName: 'Сбербанк ПАО',
          savedInstrumentTicker: 'SBERP',
        },
        indicativeData: widgetProperties.indicativeData,
      },
    });
  });

  it('should send dropped instrument update immediately', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.onDropInstruments(
        {
          issKey: 'MOEX:LKOH',
          displayName: 'Лукойл',
          symbol: 'LKOH',
        },
        true,
      );
    });

    // Assert
    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 42,
        withoutSend: false,
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:LKOH',
            savedInstrumentName: 'Лукойл',
            savedInstrumentTicker: 'LKOH',
          }),
        }),
      }),
    );
  });

  it('should update instrument from public context without unbinding widget', () => {
    // Arrange
    renderFacade();

    const publicContextParams = mockUseChartPublicContext.mock.calls[0]?.[0] as PublicContextMockParams;

    jest.clearAllMocks();

    // Act
    act(() => {
      publicContextParams.setCurrInstrument('MOEX:ROSN', 'Роснефть', 'ROSN');
    });

    // Assert
    expect(mockUnbindWidgets).not.toHaveBeenCalled();

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        withoutSend: false,
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:ROSN',
            savedInstrumentName: 'Роснефть',
            savedInstrumentTicker: 'ROSN',
          }),
        }),
      }),
    );
  });

  it('should use instrument id as name and ticker when metadata is unavailable', () => {
    // Arrange
    renderFacade();

    const corpActionsListener = mockCommunicatorListen.mock.calls.find(
      ([options]) => options.messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
    )?.[1] as (message: Record<number, string>) => void;

    // Act
    act(() => {
      corpActionsListener({
        42: 'MOEX:UNKNOWN',
      });
    });

    // Assert
    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:UNKNOWN',
            savedInstrumentName: 'MOEX:UNKNOWN',
            savedInstrumentTicker: 'MOEX:UNKNOWN',
          }),
        }),
      }),
    );
  });

  it('should not update instrument when modal selection is empty', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([]);
    });

    // Assert
    expect(mockAddContentPropsToWidget).not.toHaveBeenCalled();
    expect(mockUnbindWidgets).not.toHaveBeenCalled();
    expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();
  });

  it('should update widget highlight state', () => {
    // Arrange
    const { result } = renderFacade();

    const highlighterListener = mockCommunicatorListen.mock.calls.find(
      ([options]) => options.messageType === HIGHLIGHT_WIDGET_EVENT,
    )?.[1] as (message: Record<number, boolean>) => void;

    // Act
    act(() => {
      highlighterListener({
        42: true,
      });
    });

    // Assert
    expect(result.current.isOver).toBe(true);
  });

  it('should unsubscribe communicator listeners on unmount', () => {
    // Arrange
    const { unmount } = renderFacade();

    // Act
    unmount();

    // Assert
    expect(mockUnsubscribeCorpActions).toHaveBeenCalledTimes(1);
    expect(mockUnsubscribeHighlighter).toHaveBeenCalledTimes(1);
  });
});


import { renderHook } from '@testing-library/react';

import { useAppSelect } from '@hooks/useAppSelector';
import { useContracts } from '@modules/contracts';
import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';

import { useChartPublicContext } from '../hooks/useChartPublicContext';

import type { Contract } from '@modules/contracts';

// Mock the dependencies
jest.mock('@hooks/useAppSelector');
jest.mock('@modules/contracts');
jest.mock('@utils/filterByUniqIssKey');

jest.mock('@api/index', () => ({
  updateMenuLocked: jest.fn(),
  widgetsController: {
    delete: jest.fn(),
  },
  widgetPropertiesController: {
    update: jest.fn(),
  },
  workspaceController: {
    update: jest.fn(),
  },
}));

jest.mock('@api/controllers/workspace', () => ({
  workspaceController: {
    update: jest.fn(),
  },
}));

describe('useChartPublicContext', () => {
  const mockUseAppSelect = useAppSelect as jest.Mock;
  const mockUseContracts = useContracts as jest.Mock;
  const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;

  const mockContracts = [
    {
      issKey: DEFAULT_SYMBOL,
      displayName: 'Инструмент по умолчанию',
      symbol: 'DEFAULT',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST1',
      displayName: 'Test Instrument 1',
      symbol: 'TEST1',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST2',
      displayName: 'Test Instrument 2',
      symbol: 'TEST2',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST3',
      displayName: 'Test Instrument 3',
      symbol: 'TEST3',
      // ... other contract properties
    },
  ] as Contract[];

  const mockWidget = {
    id: 1,
    name: 'Test Widget',
    type: 'graphic',
    master: 123,
    externalProperties: [
      {
        key: 'instrument',
        value: 'MOEX:TEST1',
      },
    ],
    // ... other widget properties
  };

  const mockPublicContext = {
    instrument: 'MOEX:TEST1',
  };

  beforeEach(() => {
    jest.clearAllMocks();

    // Mock the useAppSelect to return our test data
    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }

      if (selector.toString().includes('widgets')) {
        return mockWidget;
      }

      return {};
    });

    // Mock useContracts to return our test contracts
    mockUseContracts.mockReturnValue({
      contracts: mockContracts,
    });

    // Mock filterByUniqIssKey to return the same contracts
    mockFilterByUniqIssKey.mockImplementation((contracts: Contract[]) => contracts);
  });

  it('should return the correct issKey when a matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
  });

  it('should pass instrument id, display name and ticker when a matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
    expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2', 'Test Instrument 2', 'TEST2');
  });

  it('should return undefined when no matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
    expect(mockSetCurrInstrument).not.toHaveBeenCalled();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should set default instrument with display name and ticker when context value is empty', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
    expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL, 'Инструмент по умолчанию', 'DEFAULT');
  });

  it('should resolve instrument when widget is not found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }

      if (selector.toString().includes('widgets')) {
        return null;
      }

      return {};
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 999,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
    expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST1', 'Test Instrument 1', 'TEST1');
  });

  it('should not set instrument when contracts array is empty', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    mockUseContracts.mockReturnValue({
      contracts: [],
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
    expect(mockSetCurrInstrument).not.toHaveBeenCalled();
  });
});


import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';
import { candleToBar } from '@utils/candleToBar';

import { requestBars, requestRealtimeBars } from '../requestBars';
import { isIndicativeTicker } from '../utils/isIndicativeTicker';

// Mock the dependencies
jest.mock('../utils/isIndicativeTicker');
jest.mock('@utils/candleToBar');

// Mock the api module
jest.mock('@api/index', () => ({
  __esModule: true,
  default: {
    getBars: jest.fn(),
  },
}));

// Mock the indicativeQuotesController module
jest.mock('@api/controllers/indicativeQuotesController', () => ({
  indicativeQuotesController: {
    getCandles: jest.fn(),
  },
}));

describe('requestBars', () => {
  const mockPeriodParams = {
    countBack: 100,
    from: 1640195200,
    to: 1640995200, // 2022-01-01T00:00:00Z
    firstDataRequest: true,
  };

  const mockCandle = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    ticker: 'TEST',
    volume: 1000,
    end: '2022-01-01T10:00:00Z',
    interval: '1',
  };

  const mockBar = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    volume: 1000,
    time: 1640995200000,
  };

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

  it('should handle indicative instrument correctly when indicativeData matches ticker', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockIndicativeData = {
      key: 'test-ticker',
      secId: 'string',
      instrumentName: 'string',
      settlement: 'string',
      firmName: 'string',
    };

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'test-ticker',
      indicativeData: mockIndicativeData,
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should handle indicative instrument correctly when ticker contains indicative board', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should handle non-indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: '2022-01-01%2000:00:00',
      interval: '1',
      count: mockPeriodParams.countBack,
      ticker: 'TEST',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should call onHistoryCallback with data when indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([mockBar], { noData: false });
  });

  it('should call onHistoryCallback with empty array and noData flag when indicative instrument fails', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    (
      indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
    ).mockRejectedValue(new Error('API Error'));

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should call onHistoryCallback with empty array and noData flag when non-indicative instrument fails', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    (api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
      new Error('API Error'),
    );

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should handle multiple candles for indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockCandles = [
      { ...mockCandle, end: '2022-01-01T10:00:00Z' },
      { ...mockCandle, end: '2022-01-01T11:00:00Z' },
      { ...mockCandle, end: '2022-01-01T12:00:00Z' },
    ];

    const mockResponse = {
      data: {
        indicativeCandles: mockCandles,
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(candleToBar).toHaveBeenCalledTimes(3);
    expect(result).toEqual([mockBar, mockBar, mockBar]);
  });

  it('should handle multiple candles for non-indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockCandles = [
      { ...mockCandle, end: '2022-01-01T10:00:00Z' },
      { ...mockCandle, end: '2022-01-01T11:00:00Z' },
      { ...mockCandle, end: '2022-01-01T12:00:00Z' },
    ];

    const mockResponse = {
      data: mockCandles,
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: '2022-01-01%2000:00:00',
      interval: '1',
      count: mockPeriodParams.countBack,
      ticker: 'TEST',
    });
    expect(candleToBar).toHaveBeenCalledTimes(3);
    expect(result).toEqual([mockBar, mockBar, mockBar]);
  });

  it('should handle empty indicativeCandles array', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    const mockResponse = {
      data: {
        indicativeCandles: [],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should handle empty bars array for non-indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    const mockResponse = {
      data: [],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });
});

describe('requestRealtimeBars', () => {
  const mockCandle = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    ticker: 'TEST',
    volume: 1000,
    end: '2022-01-01T10:00:00Z',
    interval: '1',
  };

  const mockBar = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    volume: 1000,
    time: 1640995200000,
  };

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

  it('should handle indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: 1,
      key: 'USD/RUB',
      date: expect.any(String), // We can't predict the exact date string
      interval: '1',
    });
    expect(candleToBar).toHaveBeenCalledWith(mockCandle);
    expect(result).toEqual(mockBar);
  });

  it('should handle non-indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: expect.any(String), // We can't predict the exact date string
      interval: '1',
      count: 1,
      ticker: 'TEST',
    });
    expect(candleToBar).toHaveBeenCalledWith(mockCandle);
    expect(result).toEqual(mockBar);
  });

  it('should handle empty indicativeCandles array in realtime request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    const mockResponse = {
      data: {
        indicativeCandles: [],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(result).toBeUndefined();
  });

  it('should handle empty bars array in realtime request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    const mockResponse = {
      data: [],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
    });

    // Assert
    expect(result).toBeUndefined();
  });

  it('should call onRealtimeCallback with bar when indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockRealtimeCallback = jest.fn();

    // Act
    await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
      onRealtimeCallback: mockRealtimeCallback,
    });

    // Assert
    expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
  });

  it('should call onRealtimeCallback with bar when non-indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    const mockRealtimeCallback = jest.fn();

    // Act
    await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
      onRealtimeCallback: mockRealtimeCallback,
    });

    // Assert
    expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
  });

  it('should handle error in indicative instrument request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    (
      indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
    ).mockRejectedValue(new Error('API Error'));

    const mockRealtimeCallback = jest.fn();

    // Act & Assert
    await expect(
      requestRealtimeBars({
        currencyPair: 'USD/RUB',
        interval: '1',
        ticker: 'TEST.indicative_spot',
        onRealtimeCallback: mockRealtimeCallback,
      }),
    ).resolves.toBeUndefined();
  });

  it('should handle error in non-indicative instrument request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    (api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
      new Error('API Error'),
    );

    const mockRealtimeCallback = jest.fn();

    // Act & Assert
    await expect(
      requestRealtimeBars({
        currencyPair: 'USD/RUB',
        interval: '1',
        ticker: 'TEST',
        onRealtimeCallback: mockRealtimeCallback,
      }),
    ).resolves.toBeUndefined();
  });
});


import { act, render } from '@testing-library/react';

import React from 'react';

import { CompareModal } from '../components/MoexChart/components/CompareModal';
import { SymbolSearchModal } from '../components/MoexChart/components/SymbolSearchModal';
import { useMoexChart } from '../components/MoexChart/hooks';
import MoexChartComponent from '../components/MoexChart/MoexChart';

import type { Contract } from '@modules/contracts';
import type { __CompareManager__ } from 'moex-chart';
import type { MutableRefObject } from 'react';

jest.mock('../components/MoexChart/hooks', () => ({
  useMoexChart: jest.fn(),
}));

jest.mock('../components/MoexChart/components/CompareModal', () => ({
  CompareModal: jest.fn(() => null),
}));

jest.mock('../components/MoexChart/components/SymbolSearchModal', () => ({
  SymbolSearchModal: jest.fn(() => null),
}));

interface CompareModalMockProps {
  widgetId: number;
  isOpen: boolean;
  setOpen: (isOpen: boolean) => void;
  onClose: () => void;
  compareManager: MutableRefObject<__CompareManager__ | null>;
}

interface SymbolSearchModalMockProps {
  widgetId: number;
  isOpen: boolean;
  setOpen: (isOpen: boolean) => void;
  onSymbolChange: (instrument: Contract) => void;
}

describe('MoexChart', () => {
  const mockUseMoexChart = useMoexChart as jest.Mock;
  const mockCompareModal = CompareModal as jest.Mock;
  const mockSymbolSearchModal = SymbolSearchModal as jest.Mock;

  const mockSetIsCompareOpen = jest.fn();
  const mockSetIsSymbolSearchOpen = jest.fn();
  const mockAddInstrumentFromModal = jest.fn();

  let containerRef: MutableRefObject<HTMLDivElement | null>;
  let compareManagerRef: MutableRefObject<__CompareManager__ | null>;

  const renderComponent = () =>
    render(
      <MoexChartComponent
        symbol="MXSE:TQBR:SBER"
        symbolName="Сбербанк"
        symbolTicker="SBER"
        widgetId={42}
        addInstrumentFromModal={mockAddInstrumentFromModal}
      />,
    );

  beforeEach(() => {
    jest.clearAllMocks();

    containerRef = {
      current: null,
    };

    compareManagerRef = {
      current: null,
    };

    mockUseMoexChart.mockReturnValue({
      containerRef,
      isCompareOpen: false,
      isSymbolSearchOpen: false,
      compareManagerRef,
      setIsCompareOpen: mockSetIsCompareOpen,
      setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
      saveSnapshot: jest.fn(),
      applySnapshot: jest.fn(),
      hasSavedSnapshot: false,
    });
  });

  it('should initialize chart hook with symbol, name and ticker', () => {
    // Arrange & Act
    renderComponent();

    // Assert
    expect(mockUseMoexChart).toHaveBeenCalledTimes(1);
    expect(mockUseMoexChart).toHaveBeenCalledWith({
      symbol: 'MXSE:TQBR:SBER',
      symbolName: 'Сбербанк',
      symbolTicker: 'SBER',
      indicativeData: undefined,
    });
  });

  it('should attach chart container ref', () => {
    // Arrange & Act
    renderComponent();

    // Assert
    expect(containerRef.current).toBeInstanceOf(HTMLDivElement);
  });

  it('should not render modals when they are closed', () => {
    // Arrange & Act
    renderComponent();

    // Assert
    expect(mockCompareModal).not.toHaveBeenCalled();
    expect(mockSymbolSearchModal).not.toHaveBeenCalled();
  });

  it('should render compare modal with chart manager', () => {
    // Arrange
    mockUseMoexChart.mockReturnValue({
      containerRef,
      isCompareOpen: true,
      isSymbolSearchOpen: false,
      compareManagerRef,
      setIsCompareOpen: mockSetIsCompareOpen,
      setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
      saveSnapshot: jest.fn(),
      applySnapshot: jest.fn(),
      hasSavedSnapshot: false,
    });

    // Act
    renderComponent();

    const compareModalProps = mockCompareModal.mock.calls[0]?.[0] as CompareModalMockProps;

    // Assert
    expect(compareModalProps.widgetId).toBe(42);
    expect(compareModalProps.isOpen).toBe(true);
    expect(compareModalProps.setOpen).toBe(mockSetIsCompareOpen);
    expect(compareModalProps.compareManager).toBe(compareManagerRef);
  });

  it('should close compare modal through onClose callback', () => {
    // Arrange
    mockUseMoexChart.mockReturnValue({
      containerRef,
      isCompareOpen: true,
      isSymbolSearchOpen: false,
      compareManagerRef,
      setIsCompareOpen: mockSetIsCompareOpen,
      setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
      saveSnapshot: jest.fn(),
      applySnapshot: jest.fn(),
      hasSavedSnapshot: false,
    });

    renderComponent();

    const compareModalProps = mockCompareModal.mock.calls[0]?.[0] as CompareModalMockProps;

    // Act
    act(() => {
      compareModalProps.onClose();
    });

    // Assert
    expect(mockSetIsCompareOpen).toHaveBeenCalledWith(false);
  });

  it('should render symbol search modal', () => {
    // Arrange
    mockUseMoexChart.mockReturnValue({
      containerRef,
      isCompareOpen: false,
      isSymbolSearchOpen: true,
      compareManagerRef,
      setIsCompareOpen: mockSetIsCompareOpen,
      setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
      saveSnapshot: jest.fn(),
      applySnapshot: jest.fn(),
      hasSavedSnapshot: false,
    });

    // Act
    renderComponent();

    const symbolSearchModalProps = mockSymbolSearchModal.mock.calls[0]?.[0] as SymbolSearchModalMockProps;

    // Assert
    expect(symbolSearchModalProps.widgetId).toBe(42);
    expect(symbolSearchModalProps.isOpen).toBe(true);
    expect(symbolSearchModalProps.setOpen).toBe(mockSetIsSymbolSearchOpen);
  });

  it('should pass selected instrument from symbol search modal', () => {
    // Arrange
    mockUseMoexChart.mockReturnValue({
      containerRef,
      isCompareOpen: false,
      isSymbolSearchOpen: true,
      compareManagerRef,
      setIsCompareOpen: mockSetIsCompareOpen,
      setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
      saveSnapshot: jest.fn(),
      applySnapshot: jest.fn(),
      hasSavedSnapshot: false,
    });

    renderComponent();

    const symbolSearchModalProps = mockSymbolSearchModal.mock.calls[0]?.[0] as SymbolSearchModalMockProps;

    const instrument = {
      issKey: 'MXSE:TQBR:GAZP',
      displayName: 'Газпром',
      symbol: 'GAZP',
    } as Contract;

    // Act
    act(() => {
      symbolSearchModalProps.onSymbolChange(instrument);
    });

    // Assert
    expect(mockAddInstrumentFromModal).toHaveBeenCalledTimes(1);
    expect(mockAddInstrumentFromModal).toHaveBeenCalledWith([instrument]);
  });
});


import type { Timeframes as TimeframesType } from 'moex-chart';

type DataSourceProvideModule = typeof import('@widgets/Chart/components/MoexChart/dataSourceProvide');

const mockRequestBars = jest.fn();
const mockRequestRealtimeBars = jest.fn();
const mockMoexChartTimeConverter = jest.fn();
const mockParseTimeframe = jest.fn();
const mockGetStartTime = jest.fn();
const mockMoexChartToIssTimeframe = jest.fn();
const mockGetISSOddTimeframePart = jest.fn();

// Mock the dependencies
jest.mock('moex-chart', () => ({
  Timeframes: {
    '10s': '10s',
    '1m': '1m',
    '5m': '5m',
  },
  parseTimeframe: mockParseTimeframe,
  getStartTime: mockGetStartTime,
}));

jest.mock('@utils/chartToReqTimeConverter', () => ({
  moexChartTimeConverter: mockMoexChartTimeConverter,
  moexChartToIssTimeframe: mockMoexChartToIssTimeframe,
  getISSOddTimeframePart: mockGetISSOddTimeframePart,
}));

jest.mock('../requestBars', () => ({
  requestBars: mockRequestBars,
  requestRealtimeBars: mockRequestRealtimeBars,
}));

jest.mock('@widgets/Chart/requestBars', () => ({
  requestBars: mockRequestBars,
  requestRealtimeBars: mockRequestRealtimeBars,
}));

const { DataSourceProvider } = jest.requireActual(
  '@widgets/Chart/components/MoexChart/dataSourceProvide',
) as DataSourceProvideModule;

const Timeframes = {
  '10s': '10s' as TimeframesType,
  '1m': '1m' as TimeframesType,
  '5m': '5m' as TimeframesType,
};

const flushPromises = async (): Promise<void> => {
  await Promise.resolve();
  await Promise.resolve();
};

describe('DataSourceProvider', () => {
  const mockBar = {
    time: 1640995200,
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    volume: 1000,
  };

  const baseTime = Date.parse('2026-05-19T10:00:00Z');

  const createBar = (minute: number, overrides: Partial<typeof mockBar> = {}) => ({
    time: baseTime + minute * 60_000,
    open: 100 + minute,
    close: 101 + minute,
    high: 102 + minute,
    low: 99 - minute,
    volume: minute + 1,
    ...overrides,
  });

  const configureFiveMinuteConvolution = (): void => {
    mockMoexChartToIssTimeframe.mockReturnValue(Timeframes['1m']);
    mockParseTimeframe.mockImplementation((timeframe: TimeframesType) =>
      timeframe === Timeframes['5m']
        ? {
            candleWidth: 5,
            dayjsUnit: 'minute',
          }
        : {
            candleWidth: 1,
            dayjsUnit: 'minute',
          },
    );
    mockGetStartTime.mockImplementation((timeframe: TimeframesType, time: number) => {
      const intervalMs = timeframe === Timeframes['5m'] ? 5 * 60_000 : 60_000;

      return (Math.floor(time / intervalMs) * intervalMs) / 1000;
    });
  };

  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2026-05-19T10:00:00Z'));

    mockMoexChartTimeConverter.mockReturnValue('1');
    mockMoexChartToIssTimeframe.mockImplementation((timeframe: TimeframesType) => timeframe);
    mockGetISSOddTimeframePart.mockReturnValue({
      value: 0,
      unit: 'minute',
    });
    mockParseTimeframe.mockReturnValue({
      candleWidth: 1,
      dayjsUnit: 'minute',
    });
    mockGetStartTime.mockImplementation((_timeframe: TimeframesType, time: number) => Math.floor(time / 1000));
  });

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

  it('should request chart history data with converted timeframe', async () => {
    // Arrange
    const mockTimeframeCallback = jest.fn();

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();

    const dataSource = provider.getDataSource(undefined, mockTimeframeCallback);

    // Act
    const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');

    // Assert
    expect(mockTimeframeCallback).toHaveBeenCalledWith(Timeframes['1m']);
    expect(mockMoexChartTimeConverter).toHaveBeenCalledWith(Timeframes['1m']);
    expect(mockRequestBars).toHaveBeenCalledWith({
      currencyPair: 'MOEX.SBER',
      interval: '1',
      periodParams: {
        firstDataRequest: true,
        to: Math.round(Date.now() / 1000),
        from: Date.now(),
        countBack: 2000,
      },
      ticker: 'MOEX:SBER',
      indicativeData: undefined,
    });
    expect(result).toEqual([mockBar]);
  });

  it('should request chart history data with indicative data', async () => {
    // Arrange
    const indicativeData = {
      id: 1,
      title: 'Test instrument',
      secId: 'SBER',
      instrumentName: 'SBER',
      settlement: 'TQBR',
      firmName: 'Test firm',
      key: 'SBER_TBQR',
    };

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();

    const dataSource = provider.getDataSource(indicativeData);

    // Act
    await dataSource(Timeframes['1m'], 'MOEX:SBER');

    // Assert
    expect(mockRequestBars).toHaveBeenCalledWith(
      expect.objectContaining({
        indicativeData,
      }),
    );
  });

  it('should use until time when it is provided', async () => {
    // Arrange
    mockMoexChartTimeConverter.mockReturnValue('5');
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();

    const dataSource = provider.getDataSource();

    const until = { time: 111 } as NonNullable<Parameters<typeof dataSource>[2]>;

    // Act
    await dataSource(Timeframes['5m'], 'MOEX:GAZP', until);

    // Assert
    expect(mockRequestBars).toHaveBeenCalledWith(
      expect.objectContaining({
        periodParams: expect.objectContaining({
          to: 111,
        }),
      }),
    );
  });

  it('should return null when history data is empty', async () => {
    // Arrange
    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestBars.mockResolvedValue([]);

    const provider = new DataSourceProvider();

    const dataSource = provider.getDataSource();

    // Act
    const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');

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

  it('should request realtime data and update normalized symbol', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    // mockMoexChartTimeConverter.mockReturnValue('1');
    const localMockedBar = {
      time: 1640995200,
      open: 100,
      close: 110,
      high: 120,
      low: 90,
      volume: Math.floor(Math.random() * 1000),
    };
    mockRequestRealtimeBars.mockImplementation(() => localMockedBar);

    provider.startRealtime({
      getSymbols: () => [' moex:sber '],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    // Assert
    expect(mockRequestRealtimeBars).toHaveBeenCalledWith({
      currencyPair: 'moex.sber',
      interval: '1',
      ticker: 'moex:sber',
      indicativeData: undefined,
    });
    expect(mockUpdate).toHaveBeenCalledWith('moex:sber', localMockedBar);
  });

  it('should request realtime data with indicative data', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();
    const indicativeData = {
      id: 1,
      title: 'Test instrument',
      secId: 'SBER',
      instrumentName: 'SBER',
      settlement: 'TQBR',
      firmName: 'Test firm',
      key: 'SBER_TBQR',
    };

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
      indicativeData,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    // Assert
    expect(mockRequestRealtimeBars).toHaveBeenCalledWith(
      expect.objectContaining({
        indicativeData,
      }),
    );
  });

  it('should not call update when realtime data is empty', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestRealtimeBars.mockResolvedValue(undefined);

    provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    // Assert
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('should not request realtime data when symbols list is empty', async () => {
    // Arrange
    const provider = new DataSourceProvider();

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    provider.startRealtime({
      getSymbols: () => [],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    // Assert
    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should clear realtime timer on unsubscribe', async () => {
    // Arrange
    const provider = new DataSourceProvider();

    // mockMoexChartTimeConverter.mockReturnValue('1');
    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    // Act
    unsubscribe();
    jest.advanceTimersByTime(1000);
    await flushPromises();

    // Assert
    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should convolve history data when ISS timeframe differs', async () => {
    // Arrange
    configureFiveMinuteConvolution();

    const historyData = Array.from({ length: 11 }, (_, minute) => createBar(minute));

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    // Act
    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    // Assert
    expect(result).toEqual([
      {
        time: baseTime + 5 * 60_000,
        open: 105,
        close: 110,
        high: 111,
        low: 90,
        volume: 40,
      },
    ]);
  });

  it('should convolve incomplete history candle group', async () => {
    // Arrange
    configureFiveMinuteConvolution();

    const historyData = Array.from({ length: 8 }, (_, minute) => createBar(minute));

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    // Act
    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    // Assert
    expect(result).toEqual([
      {
        time: baseTime + 5 * 60_000,
        open: 105,
        close: 108,
        high: 109,
        low: 92,
        volume: 21,
      },
    ]);
  });

  it('should stop candle group when candle is outside current interval', async () => {
    // Arrange
    configureFiveMinuteConvolution();

    const historyData = [
      createBar(0),
      createBar(1),
      createBar(2),
      createBar(3),
      createBar(4),
      createBar(5),
      createBar(11),
      createBar(12),
    ];

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    // Act
    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    // Assert
    expect(result).toEqual([
      {
        time: baseTime + 5 * 60_000,
        open: 105,
        close: 112,
        high: 113,
        low: 88,
        volume: 18,
      },
    ]);
  });

  it('should return empty data when convolution start index is not found', async () => {
    // Arrange
    configureFiveMinuteConvolution();

    mockGetStartTime.mockReturnValue(1);
    mockRequestBars.mockResolvedValue([createBar(0), createBar(1)]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    // Act
    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    // Assert
    expect(result).toEqual([]);
  });

  it('should append, replace and reset realtime candles during convolution', async () => {
    // Arrange
    configureFiveMinuteConvolution();

    mockRequestBars.mockResolvedValue(Array.from({ length: 11 }, (_, minute) => createBar(minute)));

    const provider = new DataSourceProvider();

    await provider.getDataSource()(Timeframes['5m'], 'MOEX:SBER');

    const firstCandle = {
      time: baseTime + 5 * 60_000,
      open: 100,
      high: 103,
      low: 99,
      close: 102,
      volume: 10,
    };

    const secondCandle = {
      time: baseTime + 6 * 60_000,
      open: 102,
      high: 106,
      low: 98,
      close: 105,
      volume: 20,
    };

    const updatedSecondCandle = {
      ...secondCandle,
      high: 107,
      low: 97,
      close: 106,
      volume: 25,
    };

    const nextTimeframeCandle = {
      time: baseTime + 10 * 60_000,
      open: 106,
      high: 108,
      low: 105,
      close: 107,
      volume: 30,
    };

    mockRequestRealtimeBars
      .mockResolvedValueOnce(firstCandle)
      .mockResolvedValueOnce(secondCandle)
      .mockResolvedValueOnce(updatedSecondCandle)
      .mockResolvedValueOnce(nextTimeframeCandle);

    const mockUpdate = jest.fn();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['5m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    jest.advanceTimersByTime(1000);
    await flushPromises();

    jest.advanceTimersByTime(1000);
    await flushPromises();

    jest.advanceTimersByTime(1000);
    await flushPromises();

    unsubscribe();

    // Assert
    expect(mockUpdate).toHaveBeenNthCalledWith(1, 'MOEX:SBER', firstCandle);
    expect(mockUpdate).toHaveBeenNthCalledWith(2, 'MOEX:SBER', {
      time: firstCandle.time,
      open: firstCandle.open,
      high: 106,
      low: 98,
      close: 105,
      volume: 30,
    });
    expect(mockUpdate).toHaveBeenNthCalledWith(3, 'MOEX:SBER', {
      time: firstCandle.time,
      open: firstCandle.open,
      high: 107,
      low: 97,
      close: 106,
      volume: 35,
    });
    expect(mockUpdate).toHaveBeenNthCalledWith(4, 'MOEX:SBER', nextTimeframeCandle);
  });

  it('should update distinct realtime candles without convolution', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const firstCandle = {
      ...mockBar,
      close: 110,
    };

    const secondCandle = {
      ...mockBar,
      close: 111,
    };

    mockRequestRealtimeBars.mockResolvedValueOnce(firstCandle).mockResolvedValueOnce(secondCandle);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    jest.advanceTimersByTime(1000);
    await flushPromises();

    unsubscribe();

    // Assert
    expect(mockUpdate).toHaveBeenNthCalledWith(1, 'MOEX:SBER', firstCandle);
    expect(mockUpdate).toHaveBeenNthCalledWith(2, 'MOEX:SBER', secondCandle);
  });

  it('should not update chart for duplicated realtime candle', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const realtimeCandle = {
      time: 1640995200,
      open: 100,
      close: 110,
      high: 120,
      low: 90,
      volume: 1000,
    };

    mockRequestRealtimeBars.mockResolvedValue(realtimeCandle);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    jest.advanceTimersByTime(1000);
    await flushPromises();

    unsubscribe();

    // Assert
    expect(mockRequestRealtimeBars).toHaveBeenCalledTimes(2);
    expect(mockUpdate).toHaveBeenCalledTimes(1);
    expect(mockUpdate).toHaveBeenCalledWith('MOEX:SBER', realtimeCandle);
  });

  it('should not request realtime data for empty normalized symbol', async () => {
    // Arrange
    const provider = new DataSourceProvider();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['   '],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    unsubscribe();

    // Assert
    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should replace existing realtime timer when startRealtime is called again', async () => {
    // Arrange
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    // Act
    jest.advanceTimersByTime(1000);
    await flushPromises();

    unsubscribe();

    // Assert
    expect(mockRequestRealtimeBars).toHaveBeenCalledTimes(1);
    expect(mockUpdate).toHaveBeenCalledTimes(1);
  });
});


import { act, render } from '@testing-library/react';

import { CompareMode } from 'moex-chart';
import React from 'react';

import { InstrumentSearch } from '@components/InstrumentSearch';

import { CompareModal } from '../components/MoexChart/components/CompareModal';

import type { Contract } from '@modules/contracts';
import type { __CompareManager__ } from 'moex-chart';
import type { MutableRefObject } from 'react';

jest.mock('moex-chart', () => ({
  __esModule: true,
  CompareMode: {
    Percentage: 'PCT',
    NewScale: 'SCALE',
    NewPane: 'PANE',
  },
}));

jest.mock('@components/InstrumentSearch', () => ({
  InstrumentSearch: jest.fn(() => null),
}));

interface CompareActions {
  handlePercent: (instrument: Contract) => void;
  handleNewScale: (instrument: Contract) => void;
  handleNewPanel: (instrument: Contract) => void;
}

interface InstrumentSearchMockProps {
  widgetId: number;
  variant: string;
  isOpen: boolean;
  setOpen: (isOpen: boolean) => void;
  isNewScaleDisabled: boolean;
  customActionsFooterHandlers: CompareActions;
}

describe('CompareModal', () => {
  const mockInstrumentSearch = InstrumentSearch as jest.Mock;

  const mockSetOpen = jest.fn();
  const mockSetSymbolMode = jest.fn();
  const mockIsNewScaleDisabled = jest.fn();
  const mockIsNewScaleDisabledObservable = jest.fn();
  const mockSubscribe = jest.fn();
  const mockUnsubscribe = jest.fn();

  let newScaleDisabledListener: ((disabled: boolean) => void) | null;
  let compareManager: __CompareManager__;
  let compareManagerRef: MutableRefObject<__CompareManager__ | null>;

  const getInstrumentSearchProps = (): InstrumentSearchMockProps => {
    const lastCall = mockInstrumentSearch.mock.calls[mockInstrumentSearch.mock.calls.length - 1];

    return lastCall?.[0] as InstrumentSearchMockProps;
  };

  const renderComponent = (isOpen = true) =>
    render(
      <CompareModal
        onClose={jest.fn()}
        widgetId={42}
        compareManager={compareManagerRef}
        isOpen={isOpen}
        setOpen={mockSetOpen}
      />,
    );

  beforeEach(() => {
    jest.clearAllMocks();

    newScaleDisabledListener = null;

    mockIsNewScaleDisabled.mockReturnValue(false);
    mockSetSymbolMode.mockResolvedValue(undefined);

    mockSubscribe.mockImplementation((listener: (disabled: boolean) => void) => {
      newScaleDisabledListener = listener;

      return {
        unsubscribe: mockUnsubscribe,
      };
    });

    mockIsNewScaleDisabledObservable.mockReturnValue({
      subscribe: mockSubscribe,
    });

    compareManager = {
      setSymbolMode: mockSetSymbolMode,
      isNewScaleDisabled: mockIsNewScaleDisabled,
      isNewScaleDisabledObservable: mockIsNewScaleDisabledObservable,
    } as unknown as __CompareManager__;

    compareManagerRef = {
      current: compareManager,
    };
  });

  it('should pass modal properties and current scale state to instrument search', () => {
    // Arrange
    mockIsNewScaleDisabled.mockReturnValue(true);

    // Act
    renderComponent();

    const instrumentSearchProps = getInstrumentSearchProps();

    // Assert
    expect(instrumentSearchProps.widgetId).toBe(42);
    expect(instrumentSearchProps.variant).toBe('single');
    expect(instrumentSearchProps.isOpen).toBe(true);
    expect(instrumentSearchProps.setOpen).toBe(mockSetOpen);
    expect(instrumentSearchProps.isNewScaleDisabled).toBe(true);
    expect(instrumentSearchProps.customActionsFooterHandlers).toEqual({
      handlePercent: expect.any(Function),
      handleNewScale: expect.any(Function),
      handleNewPanel: expect.any(Function),
    });
  });

  it('should subscribe to new scale disabled state', () => {
    // Arrange & Act
    renderComponent();

    // Assert
    expect(mockIsNewScaleDisabled).toHaveBeenCalledTimes(1);
    expect(mockIsNewScaleDisabledObservable).toHaveBeenCalledTimes(1);
    expect(mockSubscribe).toHaveBeenCalledTimes(1);
  });

  it('should update new scale disabled state from manager observable', () => {
    // Arrange
    renderComponent();

    // Act
    act(() => {
      newScaleDisabledListener?.(true);
    });

    // Assert
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(true);
  });

  it('should unsubscribe from manager observable on unmount', () => {
    // Arrange
    const { unmount } = renderComponent();

    // Act
    unmount();

    // Assert
    expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
  });

  it('should not subscribe when modal is closed', () => {
    // Arrange & Act
    renderComponent(false);

    // Assert
    expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
    expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
  });

  it('should not subscribe when compare manager is unavailable', () => {
    // Arrange
    compareManagerRef.current = null;

    // Act
    renderComponent();

    // Assert
    expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
    expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
  });

  it.each([
    ['handlePercent', CompareMode.Percentage],
    ['handleNewScale', CompareMode.NewScale],
    ['handleNewPanel', CompareMode.NewPane],
  ] as const)('should add compare instrument using %s action', (handlerName, mode) => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: 'MXSE:TQBR:SBER',
      displayName: 'Сбербанк',
      symbol: 'SBER',
    } as Contract;

    const handlers = getInstrumentSearchProps().customActionsFooterHandlers;

    // Act
    handlers[handlerName](instrument);

    // Assert
    expect(mockSetSymbolMode).toHaveBeenCalledTimes(1);
    expect(mockSetSymbolMode).toHaveBeenCalledWith(
      'Line',
      {
        symbol: 'MXSE:TQBR:SBER',
        symbolName: 'Сбербанк',
        symbolTicker: 'SBER',
      },
      mode,
    );
  });

  it('should use request symbol when display name and ticker are empty', () => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: 'MXSE:TQBR:SBER',
      displayName: '',
      symbol: '',
    } as Contract;

    // Act
    getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);

    // Assert
    expect(mockSetSymbolMode).toHaveBeenCalledWith(
      'Line',
      {
        symbol: 'MXSE:TQBR:SBER',
        symbolName: 'MXSE:TQBR:SBER',
        symbolTicker: 'MXSE:TQBR:SBER',
      },
      CompareMode.Percentage,
    );
  });

  it('should not add compare instrument without issKey', () => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: '',
      displayName: 'Сбербанк',
      symbol: 'SBER',
    } as Contract;

    // Act
    getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);

    // Assert
    expect(mockSetSymbolMode).not.toHaveBeenCalled();
  });
});