import { flatten } from 'lodash';
import { from, Observable } from 'rxjs';

import {
  DataFrame,
  DataQueryRequest,
  DataQueryResponse,
  DataSourceApi,
  DataSourceWithSupplementaryQueriesSupport,
  FieldType,
  LoadingState,
  LogLevel,
  LogsVolumeType,
  MutableDataFrame,
  SupplementaryQueryType,
  SupplementaryQueryOptions,
  toDataFrame,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';

import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query';
import { ExplorePanelData } from '../../../types';
import { mockExplorePanelData } from '../__mocks__/data';

import { getSupplementaryQueryProvider } from './supplementaryQueries';

class MockDataSourceWithSupplementaryQuerySupport
  extends MockDataSourceApi
  implements DataSourceWithSupplementaryQueriesSupport<DataQuery>
{
  private supplementaryQueriesResults: Record<SupplementaryQueryType, DataFrame[] | undefined> = {
    [SupplementaryQueryType.LogsVolume]: undefined,
    [SupplementaryQueryType.LogsSample]: undefined,
  };

  withSupplementaryQuerySupport(type: SupplementaryQueryType, data: DataFrame[]) {
    this.supplementaryQueriesResults[type] = data;
    return this;
  }

  getDataProvider(
    type: SupplementaryQueryType,
    request: DataQueryRequest<DataQuery>
  ): Observable<DataQueryResponse> | undefined {
    const data = this.supplementaryQueriesResults[type];
    if (data) {
      return from([
        { state: LoadingState.Loading, data: [] },
        { state: LoadingState.Done, data },
      ]);
    }
    return undefined;
  }

  getSupplementaryQuery(options: SupplementaryQueryOptions, query: DataQuery): DataQuery | undefined {
    return query;
  }

  getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] {
    return Object.values(SupplementaryQueryType).filter((type) => this.supplementaryQueriesResults[type]);
  }
}

const createSupplementaryQueryResponse = (type: SupplementaryQueryType, id: string) => {
  return [
    toDataFrame({
      refId: `1-${type}-${id}`,
      fields: [{ name: 'value', type: FieldType.string, values: [1] }],
      meta: {
        custom: {
          logsVolumeType: LogsVolumeType.FullRange,
        },
      },
    }),
    toDataFrame({
      refId: `2-${type}-${id}`,
      fields: [{ name: 'value', type: FieldType.string, values: [2] }],
      meta: {
        custom: {
          logsVolumeType: LogsVolumeType.FullRange,
        },
      },
    }),
  ];
};

const mockRow = (refId: string) => {
  return {
    rowIndex: 0,
    entryFieldIndex: 0,
    dataFrame: new MutableDataFrame({ refId, fields: [{ name: 'A', values: [] }] }),
    entry: '',
    hasAnsi: false,
    hasUnescapedContent: false,
    labels: {},
    logLevel: LogLevel.info,
    raw: '',
    timeEpochMs: 0,
    timeEpochNs: '0',
    timeFromNow: '',
    timeLocal: '',
    timeUtc: '',
    uid: '1',
  };
};

const mockExploreDataWithLogs = () =>
  mockExplorePanelData({
    logsResult: {
      rows: [mockRow('0'), mockRow('1')],
      visibleRange: { from: 0, to: 1 },
      bucketSize: 1000,
    },
  });

const datasources: DataSourceApi[] = [
  new MockDataSourceWithSupplementaryQuerySupport('logs-volume-a').withSupplementaryQuerySupport(
    SupplementaryQueryType.LogsVolume,
    createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-a')
  ),
  new MockDataSourceWithSupplementaryQuerySupport('logs-volume-b').withSupplementaryQuerySupport(
    SupplementaryQueryType.LogsVolume,
    createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-b')
  ),
  new MockDataSourceWithSupplementaryQuerySupport('logs-sample-a').withSupplementaryQuerySupport(
    SupplementaryQueryType.LogsSample,
    createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-a')
  ),
  new MockDataSourceWithSupplementaryQuerySupport('logs-sample-b').withSupplementaryQuerySupport(
    SupplementaryQueryType.LogsSample,
    createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-b')
  ),
  new MockDataSourceApi('no-data-providers'),
  new MockDataSourceApi('no-data-providers-2'),
];

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  getDataSourceSrv: () => {
    return {
      get: async ({ uid }: { uid: string }) => datasources.find((ds) => ds.name === uid) || undefined,
    };
  },
}));

const setup = async (targetSources: string[], type: SupplementaryQueryType) => {
  const requestMock = new MockDataQueryRequest({
    targets: targetSources.map((source, i) => new MockQuery(`${i}`, 'a', { uid: source })),
  });
  const explorePanelDataMock: Observable<ExplorePanelData> = mockExploreDataWithLogs();

  const datasources = await Promise.all(
    targetSources.map(async (source, i) => {
      const datasource = await getDataSourceSrv().get({ uid: source });
      return {
        datasource,
        targets: [new MockQuery(`${i}`, 'a', { uid: source })],
      };
    })
  );

  return getSupplementaryQueryProvider(datasources, type, requestMock, explorePanelDataMock);
};

const assertDataFrom = (type: SupplementaryQueryType, ...datasources: string[]) => {
  return flatten(
    datasources.map((name: string) => {
      return [{ refId: `1-${type}-${name}` }, { refId: `2-${type}-${name}` }];
    })
  );
};

const assertDataFromLogsResults = () => {
  return [{ meta: { custom: { logsVolumeType: LogsVolumeType.Limited } } }];
};

describe('SupplementaryQueries utils', function () {
  describe('Non-mixed data source', function () {
    it('Returns result from the provider', async () => {
      const testProvider = await setup(['logs-volume-a'], SupplementaryQueryType.LogsVolume);

      await expect(testProvider).toEmitValuesWith((received) => {
        expect(received).toMatchObject([
          { data: [], state: LoadingState.Loading },
          {
            data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
            state: LoadingState.Done,
          },
        ]);
      });
    });
    it('Uses fallback for logs volume', async () => {
      const testProvider = await setup(['no-data-providers'], SupplementaryQueryType.LogsVolume);

      await expect(testProvider).toEmitValuesWith((received) => {
        expect(received).toMatchObject([
          {
            data: assertDataFromLogsResults(),
            state: LoadingState.Done,
          },
        ]);
      });
    });
    it('Returns undefined for logs sample', async () => {
      const testProvider = await setup(['no-data-providers'], SupplementaryQueryType.LogsSample);
      await expect(testProvider).toBe(undefined);
    });
    it('Creates single fallback result', async () => {
      const testProvider = await setup(['no-data-providers', 'no-data-providers-2'], SupplementaryQueryType.LogsVolume);

      await expect(testProvider).toEmitValuesWith((received) => {
        expect(received).toMatchObject([
          {
            data: assertDataFromLogsResults(),
            state: LoadingState.Done,
          },
          {
            data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()],
            state: LoadingState.Done,
          },
        ]);
      });
    });
  });

  describe('Mixed data source', function () {
    describe('Logs volume', function () {
      describe('All data sources support full range logs volume', function () {
        it('Merges all data frames into a single response', async () => {
          const testProvider = await setup(['logs-volume-a', 'logs-volume-b'], SupplementaryQueryType.LogsVolume);
          await expect(testProvider).toEmitValuesWith((received) => {
            expect(received).toMatchObject([
              { data: [], state: LoadingState.Loading },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
                state: LoadingState.Done,
              },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a', 'logs-volume-b'),
                state: LoadingState.Done,
              },
            ]);
          });
        });
      });

      describe('All data sources do not support full range logs volume', function () {
        it('Creates single fallback result', async () => {
          const testProvider = await setup(
            ['no-data-providers', 'no-data-providers-2'],
            SupplementaryQueryType.LogsVolume
          );

          await expect(testProvider).toEmitValuesWith((received) => {
            expect(received).toMatchObject([
              {
                data: assertDataFromLogsResults(),
                state: LoadingState.Done,
              },
              {
                data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()],
                state: LoadingState.Done,
              },
            ]);
          });
        });
      });

      describe('Some data sources support full range logs volume, while others do not', function () {
        it('Creates merged result containing full range and limited logs volume', async () => {
          const testProvider = await setup(
            ['logs-volume-a', 'no-data-providers', 'logs-volume-b', 'no-data-providers-2'],
            SupplementaryQueryType.LogsVolume
          );
          await expect(testProvider).toEmitValuesWith((received) => {
            expect(received).toMatchObject([
              {
                data: [],
                state: LoadingState.Loading,
              },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
                state: LoadingState.Done,
              },
              {
                data: [
                  ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
                  ...assertDataFromLogsResults(),
                ],
                state: LoadingState.Done,
              },
              {
                data: [
                  ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'),
                  ...assertDataFromLogsResults(),
                  ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-b'),
                ],
                state: LoadingState.Done,
              },
            ]);
          });
        });
      });
    });

    describe('Logs sample', function () {
      describe('All data sources support logs sample', function () {
        it('Merges all responses into single result', async () => {
          const testProvider = await setup(['logs-sample-a', 'logs-sample-b'], SupplementaryQueryType.LogsSample);
          await expect(testProvider).toEmitValuesWith((received) => {
            expect(received).toMatchObject([
              { data: [], state: LoadingState.Loading },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'),
                state: LoadingState.Done,
              },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'),
                state: LoadingState.Done,
              },
            ]);
          });
        });
      });

      describe('All data sources do not support full range logs volume', function () {
        it('Does not provide fallback result', async () => {
          const testProvider = await setup(
            ['no-data-providers', 'no-data-providers-2'],
            SupplementaryQueryType.LogsSample
          );
          await expect(testProvider).toBeUndefined();
        });
      });

      describe('Some data sources support full range logs volume, while others do not', function () {
        it('Returns results only for data sources supporting logs sample', async () => {
          const testProvider = await setup(
            ['logs-sample-a', 'no-data-providers', 'logs-sample-b', 'no-data-providers-2'],
            SupplementaryQueryType.LogsSample
          );
          await expect(testProvider).toEmitValuesWith((received) => {
            expect(received).toMatchObject([
              { data: [], state: LoadingState.Loading },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'),
                state: LoadingState.Done,
              },
              {
                data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'),
                state: LoadingState.Done,
              },
            ]);
          });
        });
      });
    });
  });
});
