/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { waitFor } from 'spec/helpers/testing-library';
import * as actions from 'src/SqlLab/actions/sqlLab';
import { LOG_EVENT } from 'src/logger/actions';
import {
  defaultQueryEditor,
  query,
  initialState,
  queryId,
} from 'src/SqlLab/fixtures';
import { SupersetClient, isFeatureEnabled } from '@superset-ui/core';
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
import { ToastType } from '../../components/MessageToasts/types';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

jest.mock('nanoid', () => ({
  nanoid: () => 'abcd',
}));

afterAll(() => {
  jest.resetAllMocks();
});

jest.mock('@superset-ui/core', () => ({
  ...jest.requireActual('@superset-ui/core'),
  isFeatureEnabled: jest.fn(),
}));

// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getUpToDateQuery', () => {
  test('should return the up to date query editor state', () => {
    const outOfUpdatedQueryEditor = {
      ...defaultQueryEditor,
      schema: null,
      sql: 'SELECT ...',
    };
    const queryEditor = {
      ...defaultQueryEditor,
      sql: 'SELECT * FROM table',
    };
    const state = {
      sqlLab: {
        queryEditors: [queryEditor],
        unsavedQueryEditor: {},
      },
    };
    expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual(
      queryEditor,
    );
  });
});

// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('async actions', () => {
  const mockBigNumber = '9223372036854775807';
  const queryEditor = {
    ...defaultQueryEditor,
    id: 'abcd',
    autorun: false,
    latestQueryId: null,
    sql: 'SELECT *\nFROM\nWHERE',
    name: 'Untitled Query 1',
  };

  let dispatch;

  beforeEach(() => {
    dispatch = sinon.spy();
  });

  afterEach(() => fetchMock.resetHistory());

  const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
  fetchMock.get(
    fetchQueryEndpoint,
    JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }),
  );

  const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
  fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('saveQuery', () => {
    const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
    fetchMock.post(saveQueryEndpoint, { results: { json: {} } });

    const makeRequest = () => {
      const request = actions.saveQuery(query, queryId);
      return request(dispatch, () => initialState);
    };

    test('posts to the correct url', () => {
      expect.assertions(1);

      const store = mockStore(initialState);
      return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
        expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
      });
    });

    test('posts the correct query object', () => {
      const store = mockStore(initialState);
      return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
        const call = fetchMock.calls(saveQueryEndpoint)[0];
        const formData = JSON.parse(call[1].body);
        const mappedQueryToServer = actions.convertQueryToServer(query);

        Object.keys(mappedQueryToServer).forEach(key => {
          expect(formData[key]).toBeDefined();
        });
      });
    });

    test('calls 3 dispatch actions', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(dispatch.callCount).toBe(2);
      });
    });

    test('calls QUERY_EDITOR_SAVED after making a request', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(dispatch.args[0][0].type).toBe(actions.QUERY_EDITOR_SAVED);
      });
    });

    test('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
      expect.assertions(1);

      const store = mockStore(initialState);
      const expectedActionTypes = [
        actions.QUERY_EDITOR_SAVED,
        actions.QUERY_EDITOR_SET_TITLE,
      ];
      return store.dispatch(actions.saveQuery(query)).then(() => {
        expect(store.getActions().map(a => a.type)).toEqual(
          expectedActionTypes,
        );
      });
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('formatQuery', () => {
    const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
    const expectedSql = 'SELECT 1';
    fetchMock.post(formatQueryEndpoint, { result: expectedSql });

    test('posts to the correct url', async () => {
      const store = mockStore(initialState);
      store.dispatch(actions.formatQuery(query, queryId));
      await waitFor(() =>
        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
      );
      expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL);
      expect(store.getActions()[0].sql).toBe(expectedSql);
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('fetchQueryResults', () => {
    const makeRequest = () => {
      const store = mockStore(initialState);
      const request = actions.fetchQueryResults(query);
      return request(dispatch, store.getState);
    };

    test('makes the fetch request', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
      });
    });

    test('calls requestQueryResults', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS);
      });
    });

    test.skip('parses large number result without losing precision', () =>
      makeRequest().then(() => {
        expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
        expect(dispatch.callCount).toBe(2);
        expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
          mockBigNumber,
        );
      }));

    test('calls querySuccess on fetch success', () => {
      expect.assertions(1);

      const store = mockStore({});
      const expectedActionTypes = [
        actions.REQUEST_QUERY_RESULTS,
        actions.QUERY_SUCCESS,
      ];
      return store.dispatch(actions.fetchQueryResults(query)).then(() => {
        expect(store.getActions().map(a => a.type)).toEqual(
          expectedActionTypes,
        );
      });
    });

    test('calls queryFailed on fetch error', () => {
      expect.assertions(1);

      fetchMock.get(
        fetchQueryEndpoint,
        { throws: { message: 'error text' } },
        { overwriteRoutes: true },
      );

      const store = mockStore({});
      const expectedActionTypes = [
        actions.REQUEST_QUERY_RESULTS,
        actions.QUERY_FAILED,
      ];
      return store.dispatch(actions.fetchQueryResults(query)).then(() => {
        expect(store.getActions().map(a => a.type)).toEqual(
          expectedActionTypes,
        );
      });
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('runQuery without query params', () => {
    const makeRequest = () => {
      const request = actions.runQuery(query);
      return request(dispatch, () => initialState);
    };

    test('makes the fetch request', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
      });
    });

    test('calls startQuery', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(dispatch.args[0][0].type).toBe(actions.START_QUERY);
      });
    });

    test.skip('parses large number result without losing precision', () =>
      makeRequest().then(() => {
        expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
        expect(dispatch.callCount).toBe(2);
        expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
          mockBigNumber,
        );
      }));

    test('calls querySuccess on fetch success', () => {
      expect.assertions(1);

      const store = mockStore({});
      const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS];
      const { dispatch } = store;
      const request = actions.runQuery(query);
      return request(dispatch, () => initialState).then(() => {
        expect(store.getActions().map(a => a.type)).toEqual(
          expectedActionTypes,
        );
      });
    });

    test('calls queryFailed on fetch error and logs the error details', () => {
      expect.assertions(2);

      fetchMock.post(
        runQueryEndpoint,
        {
          throws: {
            message: 'error text',
            timeout: true,
            statusText: 'timeout',
          },
        },
        { overwriteRoutes: true },
      );

      const store = mockStore({});
      const expectedActionTypes = [
        actions.START_QUERY,
        LOG_EVENT,
        actions.QUERY_FAILED,
      ];
      const { dispatch } = store;
      const request = actions.runQuery(query);
      return request(dispatch, () => initialState).then(() => {
        const actions = store.getActions();
        expect(actions.map(a => a.type)).toEqual(expectedActionTypes);
        expect(actions[1].payload.eventData.issue_codes).toEqual([1000, 1001]);
      });
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('runQuery with query params', () => {
    const { location } = window;

    beforeAll(() => {
      delete window.location;
      window.location = new URL('http://localhost/sqllab/?foo=bar');
    });

    afterAll(() => {
      delete window.location;
      window.location = location;
    });

    const makeRequest = () => {
      const request = actions.runQuery(query);
      return request(dispatch, () => initialState);
    };

    test('makes the fetch request', async () => {
      const runQueryEndpointWithParams =
        'glob:*/api/v1/sqllab/execute/?foo=bar';
      fetchMock.post(
        runQueryEndpointWithParams,
        `{ "data": ${mockBigNumber} }`,
      );
      await makeRequest().then(() => {
        expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1);
      });
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('reRunQuery', () => {
    test('creates new query with a new id', () => {
      const id = 'id';
      const state = {
        sqlLab: {
          tabHistory: [id],
          queryEditors: [{ id, name: 'Dummy query editor' }],
          unsavedQueryEditor: {},
        },
      };
      const store = mockStore(state);
      const request = actions.reRunQuery(query);
      request(store.dispatch, store.getState);
      expect(store.getActions()[0].query.id).toEqual('abcd');
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('postStopQuery', () => {
    const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
    fetchMock.post(stopQueryEndpoint, {});
    const baseQuery = {
      ...query,
      id: 'test_foo',
    };

    const makeRequest = () => {
      const request = actions.postStopQuery(baseQuery);
      return request(dispatch);
    };

    test('makes the fetch request', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1);
      });
    });

    test('calls stopQuery', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        expect(dispatch.getCall(0).args[0].type).toBe(actions.STOP_QUERY);
      });
    });

    test('sends the correct data', () => {
      expect.assertions(1);

      return makeRequest().then(() => {
        const call = fetchMock.calls(stopQueryEndpoint)[0];
        const body = JSON.parse(call[1].body);
        expect(body.client_id).toBe(baseQuery.id);
      });
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('cloneQueryToNewTab', () => {
    test('creates new query editor', () => {
      expect.assertions(1);

      const id = 'id';
      const state = {
        sqlLab: {
          tabHistory: [id],
          queryEditors: [{ id, name: 'out of updated title' }],
          unsavedQueryEditor: {
            id,
            name: 'Dummy query editor',
          },
        },
      };
      const store = mockStore(state);
      const expectedActions = [
        {
          type: actions.ADD_QUERY_EDITOR,
          queryEditor: {
            name: 'Copy of Dummy query editor',
            dbId: 1,
            catalog: query.catalog,
            schema: query.schema,
            autorun: true,
            sql: 'SELECT * FROM something',
            queryLimit: undefined,
            maxRow: undefined,
            id: 'abcd',
            immutableId: 'abcd',
            templateParams: undefined,
            inLocalStorage: true,
            loaded: true,
          },
        },
      ];
      const request = actions.cloneQueryToNewTab(query, true);
      request(store.dispatch, store.getState);

      expect(store.getActions()).toEqual(expectedActions);
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('popSavedQuery', () => {
    const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
    const store = mockStore({});

    const mockSavedQueryApiResponse = {
      catalog: null,
      changed_by: {
        first_name: 'Superset',
        id: 1,
        last_name: 'Admin',
      },
      changed_on: '2024-12-28T20:06:14.246743',
      changed_on_delta_humanized: '8 days ago',
      created_by: {
        first_name: 'Superset',
        id: 1,
        last_name: 'Admin',
      },
      database: {
        database_name: 'examples',
        id: 2,
      },
      description: '',
      id: 1,
      label: 'Query 1',
      schema: 'public',
      sql: 'SELECT * FROM channels',
      sql_tables: [
        {
          catalog: null,
          schema: null,
          table: 'channels',
        },
      ],
      template_parameters: null,
    };

    const makeRequest = id => {
      const request = actions.popSavedQuery(id);
      const { dispatch } = store;

      return request(dispatch, () => initialState);
    };

    beforeEach(() => {
      supersetClientGetSpy.mockClear();
      store.clearActions();
    });

    afterAll(() => {
      supersetClientGetSpy.mockRestore();
    });

    test('calls API endpint with correct params', async () => {
      supersetClientGetSpy.mockResolvedValue({
        json: { result: mockSavedQueryApiResponse },
      });

      await makeRequest(123);

      expect(supersetClientGetSpy).toHaveBeenCalledWith({
        endpoint: '/api/v1/saved_query/123',
      });
    });

    test('dispatches addQueryEditor with correct params on successful API call', async () => {
      supersetClientGetSpy.mockResolvedValue({
        json: { result: mockSavedQueryApiResponse },
      });

      const expectedParams = {
        name: 'Query 1',
        dbId: 2,
        catalog: null,
        schema: 'public',
        sql: 'SELECT * FROM channels',
        templateParams: null,
        remoteId: 1,
      };

      await makeRequest(1);

      const addQueryEditorAction = store
        .getActions()
        .find(action => action.type === actions.ADD_QUERY_EDITOR);

      expect(addQueryEditorAction).toBeTruthy();
      expect(addQueryEditorAction?.queryEditor).toEqual(
        expect.objectContaining(expectedParams),
      );
    });

    test('should dispatch addDangerToast on API error', async () => {
      supersetClientGetSpy.mockResolvedValue(new Error());

      await makeRequest(1);

      const addToastAction = store
        .getActions()
        .find(action => action.type === ADD_TOAST);

      expect(addToastAction).toBeTruthy();
      expect(addToastAction?.payload?.toastType).toBe(ToastType.Danger);
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('addQueryEditor', () => {
    test('creates new query editor', () => {
      expect.assertions(1);

      const store = mockStore(initialState);
      const expectedActions = [
        {
          type: actions.ADD_QUERY_EDITOR,
          queryEditor: {
            ...queryEditor,
            immutableId: 'abcd',
            inLocalStorage: true,
            loaded: true,
          },
        },
      ];
      store.dispatch(actions.addQueryEditor(defaultQueryEditor));

      expect(store.getActions()).toEqual(expectedActions);
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('addNewQueryEditor', () => {
      test('creates new query editor with new tab name', () => {
        const store = mockStore({
          ...initialState,
          sqlLab: {
            ...initialState.sqlLab,
            unsavedQueryEditor: {
              id: defaultQueryEditor.id,
              name: 'Untitled Query 6',
            },
          },
        });
        const expectedActions = [
          {
            type: actions.ADD_QUERY_EDITOR,
            queryEditor: {
              id: 'abcd',
              immutableId: 'abcd',
              sql: expect.stringContaining('SELECT ...'),
              name: `Untitled Query 7`,
              dbId: defaultQueryEditor.dbId,
              catalog: defaultQueryEditor.catalog,
              schema: defaultQueryEditor.schema,
              autorun: false,
              queryLimit:
                defaultQueryEditor.queryLimit ||
                initialState.common.conf.DEFAULT_SQLLAB_LIMIT,
              inLocalStorage: true,
              loaded: true,
            },
          },
        ];
        const request = actions.addNewQueryEditor();
        request(store.dispatch, store.getState);
        expect(store.getActions()).toEqual(expectedActions);
      });
    });
  });

  test('set current query editor', () => {
    expect.assertions(1);

    const store = mockStore(initialState);
    const expectedActions = [
      {
        type: actions.SET_ACTIVE_QUERY_EDITOR,
        queryEditor: defaultQueryEditor,
      },
    ];
    store.dispatch(actions.setActiveQueryEditor(defaultQueryEditor));

    expect(store.getActions()).toEqual(expectedActions);
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('swithQueryEditor', () => {
    test('switch to the next tab editor', () => {
      const store = mockStore(initialState);
      const expectedActions = [
        {
          type: actions.SET_ACTIVE_QUERY_EDITOR,
          queryEditor: initialState.sqlLab.queryEditors[1],
        },
      ];
      store.dispatch(actions.switchQueryEditor());

      expect(store.getActions()).toEqual(expectedActions);
    });

    test('switch to the first tab editor once it reaches the rightmost tab', () => {
      const store = mockStore({
        ...initialState,
        sqlLab: {
          ...initialState.sqlLab,
          tabHistory: [
            initialState.sqlLab.queryEditors[
              initialState.sqlLab.queryEditors.length - 1
            ].id,
          ],
        },
      });
      const expectedActions = [
        {
          type: actions.SET_ACTIVE_QUERY_EDITOR,
          queryEditor: initialState.sqlLab.queryEditors[0],
        },
      ];
      store.dispatch(actions.switchQueryEditor());

      expect(store.getActions()).toEqual(expectedActions);
    });

    test('switch to the previous tab editor', () => {
      const store = mockStore({
        ...initialState,
        sqlLab: {
          ...initialState.sqlLab,
          tabHistory: [initialState.sqlLab.queryEditors[1].id],
        },
      });
      const expectedActions = [
        {
          type: actions.SET_ACTIVE_QUERY_EDITOR,
          queryEditor: initialState.sqlLab.queryEditors[0],
        },
      ];
      store.dispatch(actions.switchQueryEditor(true));

      expect(store.getActions()).toEqual(expectedActions);
    });

    test('switch to the last tab editor once it reaches the leftmost tab', () => {
      const store = mockStore({
        ...initialState,
        sqlLab: {
          ...initialState.sqlLab,
          tabHistory: [initialState.sqlLab.queryEditors[0].id],
        },
      });
      const expectedActions = [
        {
          type: actions.SET_ACTIVE_QUERY_EDITOR,
          queryEditor:
            initialState.sqlLab.queryEditors[
              initialState.sqlLab.queryEditors.length - 1
            ],
        },
      ];
      store.dispatch(actions.switchQueryEditor(true));

      expect(store.getActions()).toEqual(expectedActions);
    });
  });

  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
  describe('backend sync', () => {
    const updateTabStateEndpoint = 'glob:*/tabstateview/*';
    fetchMock.put(updateTabStateEndpoint, {});
    fetchMock.delete(updateTabStateEndpoint, {});
    fetchMock.post(updateTabStateEndpoint, JSON.stringify({ id: 1 }));

    const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*';
    fetchMock.put(updateTableSchemaEndpoint, {});
    fetchMock.delete(updateTableSchemaEndpoint, {});
    fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 }));

    const getTableMetadataEndpoint =
      'glob:**/api/v1/database/*/table_metadata/*';
    fetchMock.get(getTableMetadataEndpoint, {});
    const getExtraTableMetadataEndpoint =
      'glob:**/api/v1/database/*/table_metadata/extra/*';
    fetchMock.get(getExtraTableMetadataEndpoint, {});

    beforeEach(() => {
      isFeatureEnabled.mockImplementation(
        feature => feature === 'SQLLAB_BACKEND_PERSISTENCE',
      );
    });

    afterEach(() => {
      isFeatureEnabled.mockRestore();
    });

    afterEach(() => fetchMock.resetHistory());

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('addQueryEditor', () => {
      test('creates the tab state in the local storage', () => {
        expect.assertions(2);

        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.ADD_QUERY_EDITOR,
            queryEditor: {
              ...queryEditor,
              id: 'abcd',
              immutableId: 'abcd',
              loaded: true,
              inLocalStorage: true,
            },
          },
        ];
        store.dispatch(actions.addQueryEditor(queryEditor));

        expect(store.getActions()).toEqual(expectedActions);
        expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('removeQueryEditor', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.REMOVE_QUERY_EDITOR,
            queryEditor,
          },
        ];
        store.dispatch(actions.removeQueryEditor(queryEditor));
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetDb', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const dbId = 42;
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SETDB,
            queryEditor,
            dbId,
          },
        ];
        store.dispatch(actions.queryEditorSetDb(queryEditor, dbId));
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetCatalog', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const catalog = 'public';
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_CATALOG,
            queryEditor,
            catalog,
          },
        ];
        store.dispatch(actions.queryEditorSetCatalog(queryEditor, catalog));
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetSchema', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const schema = 'schema';
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_SCHEMA,
            queryEditor,
            schema,
          },
        ];
        store.dispatch(actions.queryEditorSetSchema(queryEditor, schema));
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetAutorun', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const autorun = true;
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_AUTORUN,
            queryEditor,
            autorun,
          },
        ];
        store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun));
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetTitle', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const name = 'name';
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_TITLE,
            queryEditor,
            name,
          },
        ];
        store.dispatch(
          actions.queryEditorSetTitle(queryEditor, name, queryEditor.id),
        );
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetAndSaveSql', () => {
      const sql = 'SELECT * ';
      const expectedActions = [
        {
          type: actions.QUERY_EDITOR_SET_SQL,
          queryEditor,
          sql,
        },
      ];
      // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
      describe('with backend persistence flag on', () => {
        test('updates the tab state in the backend', () => {
          expect.assertions(2);

          const store = mockStore({
            ...initialState,
            sqlLab: {
              ...initialState.sqlLab,
              queryEditors: [queryEditor],
            },
          });
          const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
          return request(store.dispatch, store.getState).then(() => {
            expect(store.getActions()).toEqual(expectedActions);
            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
          });
        });
      });
      // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
      describe('with backend persistence flag off', () => {
        test('does not update the tab state in the backend', () => {
          isFeatureEnabled.mockImplementation(
            feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
          );

          const store = mockStore({
            ...initialState,
            sqlLab: {
              ...initialState.sqlLab,
              queryEditors: [queryEditor],
            },
          });
          const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
          request(store.dispatch, store.getState);

          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
          isFeatureEnabled.mockRestore();
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetQueryLimit', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const queryLimit = 10;
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
            queryEditor,
            queryLimit,
          },
        ];
        store.dispatch(
          actions.queryEditorSetQueryLimit(queryEditor, queryLimit),
        );
        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('queryEditorSetTemplateParams', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(1);

        const templateParams = '{"foo": "bar"}';
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS,
            queryEditor,
            templateParams,
          },
        ];
        store.dispatch(
          actions.queryEditorSetTemplateParams(queryEditor, templateParams),
        );

        expect(store.getActions()).toEqual(expectedActions);
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('addTable', () => {
      test('dispatches table state from unsaved change', () => {
        const tableName = 'table';
        const catalogName = null;
        const schemaName = 'schema';
        const expectedDbId = 473892;
        const store = mockStore({
          ...initialState,
          sqlLab: {
            ...initialState.sqlLab,
            unsavedQueryEditor: {
              id: query.id,
              dbId: expectedDbId,
            },
          },
        });
        const request = actions.addTable(
          query,
          tableName,
          catalogName,
          schemaName,
        );
        request(store.dispatch, store.getState);
        expect(store.getActions()[0]).toEqual(
          expect.objectContaining({
            table: expect.objectContaining({
              name: tableName,
              catalog: catalogName,
              schema: schemaName,
              dbId: expectedDbId,
            }),
          }),
        );
      });

      test('uses tabViewId when available', () => {
        const tableName = 'table';
        const catalogName = null;
        const schemaName = 'schema';
        const expectedDbId = 473892;
        const tabViewId = '123';
        const queryWithTabViewId = { ...query, tabViewId };

        const store = mockStore({
          ...initialState,
          sqlLab: {
            ...initialState.sqlLab,
            unsavedQueryEditor: {
              id: query.id,
              dbId: expectedDbId,
            },
          },
        });

        const request = actions.addTable(
          queryWithTabViewId,
          tableName,
          catalogName,
          schemaName,
        );
        request(store.dispatch, store.getState);

        expect(store.getActions()[0]).toEqual(
          expect.objectContaining({
            table: expect.objectContaining({
              name: tableName,
              catalog: catalogName,
              schema: schemaName,
              dbId: expectedDbId,
              queryEditorId: tabViewId, // Should use tabViewId, not id
            }),
          }),
        );
      });

      test('falls back to id when tabViewId is not available', () => {
        const tableName = 'table';
        const catalogName = null;
        const schemaName = 'schema';
        const expectedDbId = 473892;
        const queryWithoutTabViewId = { ...query, tabViewId: undefined };

        const store = mockStore({
          ...initialState,
          sqlLab: {
            ...initialState.sqlLab,
            unsavedQueryEditor: {
              id: query.id,
              dbId: expectedDbId,
            },
          },
        });

        const request = actions.addTable(
          queryWithoutTabViewId,
          tableName,
          catalogName,
          schemaName,
        );
        request(store.dispatch, store.getState);

        expect(store.getActions()[0]).toEqual(
          expect.objectContaining({
            table: expect.objectContaining({
              name: tableName,
              catalog: catalogName,
              schema: schemaName,
              dbId: expectedDbId,
              queryEditorId: query.id, // Should use id when tabViewId is not available
            }),
          }),
        );
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('syncTable', () => {
      test('updates the table schema state in the backend', () => {
        expect.assertions(4);

        const tableName = 'table';
        const schemaName = 'schema';
        const store = mockStore(initialState);
        const expectedActionTypes = [
          actions.MERGE_TABLE, // syncTable
        ];
        const request = actions.syncTable(query, tableName, schemaName);
        return request(store.dispatch, store.getState).then(() => {
          expect(store.getActions().map(a => a.type)).toEqual(
            expectedActionTypes,
          );
          expect(store.getActions()[0].prepend).toBeFalsy();
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);

          // tab state is not updated, since no query was run
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('runTablePreviewQuery', () => {
      const results = {
        data: mockBigNumber,
        query: { sqlEditorId: 'null', dbId: 1 },
        query_id: 'efgh',
      };
      const tableName = 'table';
      const catalogName = null;
      const schemaName = 'schema';
      const store = mockStore({
        ...initialState,
        sqlLab: {
          ...initialState.sqlLab,
          databases: {
            1: { disable_data_preview: false },
          },
        },
      });

      beforeEach(() => {
        fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
          overwriteRoutes: true,
        });
      });

      afterEach(() => {
        store.clearActions();
        fetchMock.resetHistory();
      });

      test('updates and runs data preview query when configured', () => {
        expect.assertions(3);

        const expectedActionTypes = [
          actions.MERGE_TABLE, // addTable (data preview)
          actions.START_QUERY, // runQuery (data preview)
          actions.QUERY_SUCCESS, // querySuccess
        ];
        const request = actions.runTablePreviewQuery({
          dbId: 1,
          name: tableName,
          catalog: catalogName,
          schema: schemaName,
        });
        return request(store.dispatch, store.getState).then(() => {
          expect(store.getActions().map(a => a.type)).toEqual(
            expectedActionTypes,
          );
          expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
          // tab state is not updated, since the query is a data preview
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
        });
      });

      test('runs data preview query only', () => {
        const expectedActionTypes = [
          actions.START_QUERY, // runQuery (data preview)
          actions.QUERY_SUCCESS, // querySuccess
        ];
        const request = actions.runTablePreviewQuery(
          {
            dbId: 1,
            name: tableName,
            catalog: catalogName,
            schema: schemaName,
          },
          true,
        );
        return request(store.dispatch, store.getState).then(() => {
          expect(store.getActions().map(a => a.type)).toEqual(
            expectedActionTypes,
          );
          expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
          // tab state is not updated, since the query is a data preview
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('expandTable', () => {
      test('updates the table schema state in the backend', () => {
        expect.assertions(2);

        const table = { id: 1 };
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.EXPAND_TABLE,
            table,
          },
        ];
        return store.dispatch(actions.expandTable(table)).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('collapseTable', () => {
      test('updates the table schema state in the backend', () => {
        expect.assertions(2);

        const table = { id: 1 };
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.COLLAPSE_TABLE,
            table,
          },
        ];
        return store.dispatch(actions.collapseTable(table)).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('removeTables', () => {
      test('updates the table schema state in the backend', () => {
        expect.assertions(2);

        const table = { id: 1, initialized: true };
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.REMOVE_TABLES,
            tables: [table],
          },
        ];
        return store.dispatch(actions.removeTables([table])).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
        });
      });

      test('deletes multiple tables and updates the table schema state in the backend', () => {
        expect.assertions(2);

        const tables = [
          { id: 1, initialized: true },
          { id: 2, initialized: true },
        ];
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.REMOVE_TABLES,
            tables,
          },
        ];
        return store.dispatch(actions.removeTables(tables)).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
        });
      });

      test('only updates the initialized table schema state in the backend', () => {
        expect.assertions(2);

        const tables = [{ id: 1 }, { id: 2, initialized: true }];
        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.REMOVE_TABLES,
            tables,
          },
        ];
        return store.dispatch(actions.removeTables(tables)).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
        });
      });
    });

    // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
    describe('syncQueryEditor', () => {
      test('updates the tab state in the backend', () => {
        expect.assertions(3);

        const results = {
          data: mockBigNumber,
          query: { sqlEditorId: 'null' },
          query_id: 'efgh',
        };
        fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
          overwriteRoutes: true,
        });

        const oldQueryEditor = { ...queryEditor, inLocalStorage: true };
        const tables = [
          {
            id: 'one',
            dataPreviewQueryId: 'previewOne',
            queryEditorId: oldQueryEditor.id,
            inLocalStorage: true,
          },
          {
            id: 'two',
            dataPreviewQueryId: 'previewTwo',
            queryEditorId: oldQueryEditor.id,
            inLocalStorage: true,
          },
        ];
        const queries = [
          {
            ...query,
            id: 'previewOne',
            sqlEditorId: oldQueryEditor.id,
            inLocalStorage: true,
          },
          {
            ...query,
            id: 'previewTwo',
            sqlEditorId: oldQueryEditor.id,
            inLocalStorage: true,
          },
        ];
        const store = mockStore({
          sqlLab: {
            queries,
            tables,
          },
        });
        const expectedActions = [
          {
            type: actions.MIGRATE_QUERY_EDITOR,
            oldQueryEditor,
            // new qe has a different id
            newQueryEditor: {
              ...oldQueryEditor,
              tabViewId: '1',
              inLocalStorage: false,
              loaded: true,
            },
          },
          {
            type: actions.MIGRATE_TABLE,
            oldTable: tables[0],
            // new table has a different id and points to new query editor
            newTable: { ...tables[0], id: 1, queryEditorId: '1' },
          },
          {
            type: actions.MIGRATE_TABLE,
            oldTable: tables[1],
            // new table has a different id and points to new query editor
            newTable: { ...tables[1], id: 1, queryEditorId: '1' },
          },
          {
            type: actions.MIGRATE_QUERY,
            queryId: 'previewOne',
            queryEditorId: '1',
          },
          {
            type: actions.MIGRATE_QUERY,
            queryId: 'previewTwo',
            queryEditorId: '1',
          },
        ];
        return store
          .dispatch(actions.syncQueryEditor(oldQueryEditor))
          .then(() => {
            expect(store.getActions()).toEqual(expectedActions);
            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3);

            // query editor has 2 tables loaded in the schema viewer
            expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
          });
      });
    });
  });
});
