import React from 'react';
import { shallow } from 'enzyme';
import ChartClient from '@superset-ui/core/src/chart/clients/ChartClient';
import ChartDataProvider, { Props } from '@superset-ui/core/src/chart/components/ChartDataProvider';
import { bigNumberFormData } from '../fixtures/formData';

// Note: the mock implementation of these function directly affects the expected results below
const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) =>
  Promise.resolve(formData),
);

type MockLoadFormData = typeof defaultMockLoadFormData | jest.Mock<Promise<unknown>, unknown[]>;

let mockLoadFormData: MockLoadFormData = defaultMockLoadFormData;

function createPromise<T>(input: T) {
  return Promise.resolve(input);
}

function createArrayPromise<T>(input: T) {
  return Promise.resolve([input]);
}

const mockLoadDatasource = jest.fn<Promise<unknown>, unknown[]>(createPromise);
const mockLoadQueryData = jest.fn<Promise<unknown>, unknown[]>(createArrayPromise);

// ChartClient is now a mock
jest.mock('@superset-ui/core/src/chart/clients/ChartClient', () =>
  jest.fn().mockImplementation(() => ({
    loadDatasource: mockLoadDatasource,
    loadFormData: mockLoadFormData,
    loadQueryData: mockLoadQueryData,
  })),
);

const ChartClientMock = ChartClient as jest.Mock<ChartClient>;

describe('ChartDataProvider', () => {
  beforeEach(() => {
    ChartClientMock.mockClear();

    mockLoadFormData = defaultMockLoadFormData;
    mockLoadFormData.mockClear();
    mockLoadDatasource.mockClear();
    mockLoadQueryData.mockClear();
  });

  const props: Props = {
    formData: { ...bigNumberFormData },
    children: () => <div />,
  };

  function setup(overrideProps?: Partial<Props>) {
    return shallow(<ChartDataProvider {...props} {...overrideProps} />);
  }

  it('instantiates a new ChartClient()', () => {
    setup();
    expect(ChartClientMock).toHaveBeenCalledTimes(1);
  });

  describe('ChartClient.loadFormData', () => {
    it('calls method on mount', () => {
      setup();
      expect(mockLoadFormData.mock.calls).toHaveLength(1);
      expect(mockLoadFormData.mock.calls[0][0]).toEqual({
        sliceId: props.sliceId,
        formData: props.formData,
      });
    });

    it('should pass formDataRequestOptions to ChartClient.loadFormData', () => {
      const options = { host: 'override' };
      setup({ formDataRequestOptions: options });
      expect(mockLoadFormData.mock.calls).toHaveLength(1);
      expect(mockLoadFormData.mock.calls[0][1]).toEqual(options);
    });

    it('calls ChartClient.loadFormData when formData or sliceId change', () => {
      const wrapper = setup();
      const newProps = { sliceId: 123, formData: undefined };
      expect(mockLoadFormData.mock.calls).toHaveLength(1);

      wrapper.setProps(newProps);
      expect(mockLoadFormData.mock.calls).toHaveLength(2);
      expect(mockLoadFormData.mock.calls[1][0]).toEqual(newProps);
    });
  });

  describe('ChartClient.loadDatasource', () => {
    it('does not method if loadDatasource is false', () =>
      new Promise(done => {
        expect.assertions(1);
        setup({ loadDatasource: false });
        setTimeout(() => {
          expect(mockLoadDatasource.mock.calls).toHaveLength(0);
          done();
        }, 0);
      }));

    it('calls method on mount if loadDatasource is true', () =>
      new Promise(done => {
        expect.assertions(2);
        setup({ loadDatasource: true });
        setTimeout(() => {
          expect(mockLoadDatasource.mock.calls).toHaveLength(1);
          expect(mockLoadDatasource.mock.calls[0][0]).toEqual(props.formData.datasource);
          done();
        }, 0);
      }));

    it('should pass datasourceRequestOptions to ChartClient.loadDatasource', () =>
      new Promise(done => {
        expect.assertions(2);
        const options = { host: 'override' };
        setup({ loadDatasource: true, datasourceRequestOptions: options });
        setTimeout(() => {
          expect(mockLoadDatasource.mock.calls).toHaveLength(1);
          expect(mockLoadDatasource.mock.calls[0][1]).toEqual(options);
          done();
        }, 0);
      }));

    it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', () =>
      new Promise(done => {
        expect.assertions(3);
        const newDatasource = 'test';
        const wrapper = setup({ loadDatasource: true });
        wrapper.setProps({ formData: { datasource: newDatasource }, sliceId: undefined });

        setTimeout(() => {
          expect(mockLoadDatasource.mock.calls).toHaveLength(2);
          expect(mockLoadDatasource.mock.calls[0][0]).toEqual(props.formData.datasource);
          expect(mockLoadDatasource.mock.calls[1][0]).toEqual(newDatasource);
          done();
        }, 0);
      }));
  });

  describe('ChartClient.loadQueryData', () => {
    it('calls method on mount', () =>
      new Promise(done => {
        expect.assertions(2);
        setup();
        setTimeout(() => {
          expect(mockLoadQueryData.mock.calls).toHaveLength(1);
          expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData);
          done();
        }, 0);
      }));

    it('should pass queryDataRequestOptions to ChartClient.loadQueryData', () =>
      new Promise(done => {
        expect.assertions(2);
        const options = { host: 'override' };
        setup({ queryRequestOptions: options });
        setTimeout(() => {
          expect(mockLoadQueryData.mock.calls).toHaveLength(1);
          expect(mockLoadQueryData.mock.calls[0][1]).toEqual(options);
          done();
        }, 0);
      }));

    it('calls ChartClient.loadQueryData when formData or sliceId change', () =>
      new Promise(done => {
        expect.assertions(3);
        const newFormData = { key: 'test' };
        const wrapper = setup();
        wrapper.setProps({ formData: newFormData, sliceId: undefined });

        setTimeout(() => {
          expect(mockLoadQueryData.mock.calls).toHaveLength(2);
          expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData);
          expect(mockLoadQueryData.mock.calls[1][0]).toEqual(newFormData);
          done();
        }, 0);
      }));
  });

  describe('children', () => {
    it('calls children({ loading: true }) when loading', () => {
      const children = jest.fn<React.ReactNode, unknown[]>();
      setup({ children });

      // during the first tick (before more promises resolve) loading is true
      expect(children.mock.calls).toHaveLength(1);
      expect(children.mock.calls[0][0]).toEqual({ loading: true });
    });

    it('calls children({ payload }) when loaded', () =>
      new Promise(done => {
        expect.assertions(2);
        const children = jest.fn<React.ReactNode, unknown[]>();
        setup({ children, loadDatasource: true });

        setTimeout(() => {
          expect(children.mock.calls).toHaveLength(2);
          expect(children.mock.calls[1][0]).toEqual({
            payload: {
              formData: props.formData,
              datasource: props.formData.datasource,
              queriesData: [props.formData],
            },
          });
          done();
        }, 0);
      }));

    it('calls children({ error }) upon request error', () =>
      new Promise(done => {
        expect.assertions(2);
        const children = jest.fn<React.ReactNode, unknown[]>();
        mockLoadFormData = jest.fn(() => Promise.reject(new Error('error')));

        setup({ children });

        setTimeout(() => {
          expect(children.mock.calls).toHaveLength(2); // loading + error
          expect(children.mock.calls[1][0]).toEqual({ error: new Error('error') });
          done();
        }, 0);
      }));

    it('calls children({ error }) upon JS error', () =>
      new Promise(done => {
        expect.assertions(2);
        const children = jest.fn<React.ReactNode, unknown[]>();

        mockLoadFormData = jest.fn(() => {
          throw new Error('non-async error');
        });

        setup({ children });

        setTimeout(() => {
          expect(children.mock.calls).toHaveLength(2); // loading + error
          expect(children.mock.calls[1][0]).toEqual({ error: new Error('non-async error') });
          done();
        }, 0);
      }));
  });

  describe('callbacks', () => {
    it('calls onLoad(payload) when loaded', () =>
      new Promise(done => {
        expect.assertions(2);
        const onLoaded = jest.fn<void, unknown[]>();
        setup({ onLoaded, loadDatasource: true });

        setTimeout(() => {
          expect(onLoaded.mock.calls).toHaveLength(1);
          expect(onLoaded.mock.calls[0][0]).toEqual({
            formData: props.formData,
            datasource: props.formData.datasource,
            queriesData: [props.formData],
          });
          done();
        }, 0);
      }));

    it('calls onError(error) upon request error', () =>
      new Promise(done => {
        expect.assertions(2);
        const onError = jest.fn<void, unknown[]>();
        mockLoadFormData = jest.fn(() => Promise.reject(new Error('error')));

        setup({ onError });
        setTimeout(() => {
          expect(onError.mock.calls).toHaveLength(1);
          expect(onError.mock.calls[0][0]).toEqual(new Error('error'));
          done();
        }, 0);
      }));

    it('calls onError(error) upon JS error', () =>
      new Promise(done => {
        expect.assertions(2);
        const onError = jest.fn<void, unknown[]>();

        mockLoadFormData = jest.fn(() => {
          throw new Error('non-async error');
        });

        setup({ onError });
        setTimeout(() => {
          expect(onError.mock.calls).toHaveLength(1);
          expect(onError.mock.calls[0][0]).toEqual(new Error('non-async error'));
          done();
        }, 0);
      }));
  });
});
