import LokiDatasource, { RangeQueryOptions } from './datasource';
import { LokiQuery, LokiResultType, LokiResponse, LokiLegacyStreamResponse } from './types';
import { getQueryOptions } from 'test/helpers/getQueryOptions';
import { BackendSrv } from 'app/core/services/backend_srv';
import { AnnotationQueryRequest, DataSourceApi, DataFrame, dateTime, TimeRange, FieldCache } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/custom_variable';
import { makeMockLokiDatasource } from './mocks';
import { ExploreMode } from 'app/types';
import { of } from 'rxjs';
import omit from 'lodash/omit';

describe('LokiDatasource', () => {
  const instanceSettings: any = {
    url: 'myloggingurl',
  };

  const legacyTestResp: { data: LokiLegacyStreamResponse; status: number } = {
    data: {
      streams: [
        {
          entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
          labels: '{}',
        },
      ],
    },
    status: 404, // for simulating legacy endpoint
  };

  const testResp: { data: LokiResponse } = {
    data: {
      data: {
        resultType: LokiResultType.Stream,
        result: [
          {
            stream: {},
            values: [['1573646419522934000', 'hello']],
          },
        ],
      },
      status: 'success',
    },
  };

  const backendSrvMock = { datasourceRequest: jest.fn() };
  const backendSrv = (backendSrvMock as unknown) as BackendSrv;

  const templateSrvMock = ({
    getAdhocFilters: (): any[] => [],
    replace: (a: string) => a,
  } as unknown) as TemplateSrv;

  describe('when creating range query', () => {
    let ds: LokiDatasource;
    let adjustIntervalSpy: jest.SpyInstance;
    beforeEach(() => {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      const customSettings = { ...instanceSettings, jsonData: customData };
      ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval');
    });

    it('should use default intervalMs if one is not provided', () => {
      const target = { expr: '{job="grafana"}', refId: 'B' };
      const raw = { from: 'now', to: 'now-1h' };
      const range = { from: dateTime(), to: dateTime(), raw: raw };
      const options = {
        range,
      };

      const req = ds.createRangeQuery(target, options);
      expect(req.start).toBeDefined();
      expect(req.end).toBeDefined();
      expect(adjustIntervalSpy).toHaveBeenCalledWith(1000, expect.anything());
    });

    it('should use provided intervalMs', () => {
      const target = { expr: '{job="grafana"}', refId: 'B' };
      const raw = { from: 'now', to: 'now-1h' };
      const range = { from: dateTime(), to: dateTime(), raw: raw };
      const options = {
        range,
        intervalMs: 2000,
      };

      const req = ds.createRangeQuery(target, options);
      expect(req.start).toBeDefined();
      expect(req.end).toBeDefined();
      expect(adjustIntervalSpy).toHaveBeenCalledWith(2000, expect.anything());
    });
  });

  describe('when running range query with fallback', () => {
    let ds: LokiDatasource;
    beforeEach(() => {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      const customSettings = { ...instanceSettings, jsonData: customData };
      ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(legacyTestResp));
    });

    test('should try latest endpoint but fall back to legacy endpoint if it cannot be reached', async () => {
      const options = getQueryOptions<LokiQuery>({
        targets: [{ expr: '{job="grafana"}', refId: 'B' }],
        exploreMode: ExploreMode.Logs,
      });

      ds.runLegacyQuery = jest.fn();
      await ds.runRangeQueryWithFallback(options.targets[0], options).toPromise();
      expect(ds.runLegacyQuery).toBeCalled();
    });
  });

  describe('when querying', () => {
    const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, legacyTestResp);
    let ds: LokiDatasource;

    beforeEach(() => {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      const customSettings = { ...instanceSettings, jsonData: customData };
      ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
    });

    test('should run instant query and range query when in metrics mode', async () => {
      const options = getQueryOptions<LokiQuery>({
        targets: [{ expr: 'rate({job="grafana"}[5m])', refId: 'A' }],
        exploreMode: ExploreMode.Metrics,
      });

      ds.runInstantQuery = jest.fn(() => of({ data: [] }));
      ds.runLegacyQuery = jest.fn();
      ds.runRangeQueryWithFallback = jest.fn(() => of({ data: [] }));
      await ds.query(options).toPromise();

      expect(ds.runInstantQuery).toBeCalled();
      expect(ds.runLegacyQuery).not.toBeCalled();
      expect(ds.runRangeQueryWithFallback).toBeCalled();
    });

    test('should just run range query when in logs mode', async () => {
      const options = getQueryOptions<LokiQuery>({
        targets: [{ expr: '{job="grafana"}', refId: 'B' }],
        exploreMode: ExploreMode.Logs,
      });

      ds.runInstantQuery = jest.fn(() => of({ data: [] }));
      ds.runRangeQueryWithFallback = jest.fn(() => of({ data: [] }));
      await ds.query(options).toPromise();

      expect(ds.runInstantQuery).not.toBeCalled();
      expect(ds.runRangeQueryWithFallback).toBeCalled();
    });

    test('should use default max lines when no limit given', () => {
      testLimit({
        expectedLimit: 1000,
      });
    });

    test('should use custom max lines if limit is set', () => {
      testLimit({
        maxLines: 20,
        expectedLimit: 20,
      });
    });

    test('should use custom maxDataPoints if set in request', () => {
      testLimit({
        maxDataPoints: 500,
        expectedLimit: 500,
      });
    });

    test('should use datasource maxLimit if maxDataPoints is higher', () => {
      testLimit({
        maxLines: 20,
        maxDataPoints: 500,
        expectedLimit: 20,
      });
    });

    test('should return series data', async () => {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      const customSettings = { ...instanceSettings, jsonData: customData };
      const ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      backendSrvMock.datasourceRequest = jest
        .fn()
        .mockReturnValueOnce(Promise.resolve(legacyTestResp))
        .mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status')));

      const options = getQueryOptions<LokiQuery>({
        targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }],
      });

      const res = await ds.query(options).toPromise();

      const dataFrame = res.data[0] as DataFrame;
      const fieldCache = new FieldCache(dataFrame);
      expect(fieldCache.getFieldByName('line').values.get(0)).toBe('hello');
      expect(dataFrame.meta.limit).toBe(20);
      expect(dataFrame.meta.searchWords).toEqual(['foo']);
    });
  });

  describe('When interpolating variables', () => {
    let ds: LokiDatasource;
    let variable: CustomVariable;

    beforeEach(() => {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      const customSettings = { ...instanceSettings, jsonData: customData };
      ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      variable = new CustomVariable({}, {} as any);
    });

    it('should only escape single quotes', () => {
      expect(ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", variable)).toEqual("abc\\\\'$^*{}[]+?.()|");
    });

    it('should return a number', () => {
      expect(ds.interpolateQueryExpr(1000, variable)).toEqual(1000);
    });

    describe('and variable allows multi-value', () => {
      beforeEach(() => {
        variable.multi = true;
      });

      it('should regex escape values if the value is a string', () => {
        expect(ds.interpolateQueryExpr('looking*glass', variable)).toEqual('looking\\\\*glass');
      });

      it('should return pipe separated values if the value is an array of strings', () => {
        expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], variable)).toEqual('a\\\\|bc|de\\\\|f');
      });
    });

    describe('and variable allows all', () => {
      beforeEach(() => {
        variable.includeAll = true;
      });

      it('should regex escape values if the array is a string', () => {
        expect(ds.interpolateQueryExpr('looking*glass', variable)).toEqual('looking\\\\*glass');
      });

      it('should return pipe separated values if the value is an array of strings', () => {
        expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], variable)).toEqual('a\\\\|bc|de\\\\|f');
      });
    });
  });

  describe('when performing testDataSource', () => {
    let ds: DataSourceApi<any, any>;
    let result: any;

    describe('and call succeeds', () => {
      beforeEach(async () => {
        const backendSrv = ({
          async datasourceRequest() {
            return Promise.resolve({
              status: 200,
              data: {
                data: ['avalue'],
              },
            });
          },
        } as unknown) as BackendSrv;
        ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
        result = await ds.testDatasource();
      });

      it('should return successfully', () => {
        expect(result.status).toBe('success');
      });
    });

    describe('and call fails with 401 error', () => {
      let ds: LokiDatasource;
      beforeEach(() => {
        backendSrvMock.datasourceRequest = jest.fn(() =>
          Promise.reject({
            statusText: 'Unauthorized',
            status: 401,
            data: {
              message: 'Unauthorized',
            },
          })
        );

        const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
        const customSettings = { ...instanceSettings, jsonData: customData };
        ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
      });

      it('should return error status and a detailed error message', async () => {
        const result = await ds.testDatasource();
        expect(result.status).toEqual('error');
        expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized');
      });
    });

    describe('and call fails with 404 error', () => {
      beforeEach(async () => {
        const backendSrv = ({
          async datasourceRequest() {
            return Promise.reject({
              statusText: 'Not found',
              status: 404,
              data: '404 page not found',
            });
          },
        } as unknown) as BackendSrv;
        ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
        result = await ds.testDatasource();
      });

      it('should return error status and a detailed error message', () => {
        expect(result.status).toEqual('error');
        expect(result.message).toBe('Loki: Not found. 404. 404 page not found');
      });
    });

    describe('and call fails with 502 error', () => {
      beforeEach(async () => {
        const backendSrv = ({
          async datasourceRequest() {
            return Promise.reject({
              statusText: 'Bad Gateway',
              status: 502,
              data: '',
            });
          },
        } as unknown) as BackendSrv;
        ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
        result = await ds.testDatasource();
      });

      it('should return error status and a detailed error message', () => {
        expect(result.status).toEqual('error');
        expect(result.message).toBe('Loki: Bad Gateway. 502');
      });
    });
  });

  describe('when creating a range query', () => {
    const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
    const query: LokiQuery = { expr: 'foo', refId: 'bar' };

    // Loki v1 API has an issue with float step parameters, can be removed when API is fixed
    it('should produce an integer step parameter', () => {
      const range: TimeRange = {
        from: dateTime(0),
        to: dateTime(1e9 + 1),
        raw: { from: '0', to: '1000000001' },
      };
      // Odd timerange/interval combination that would lead to a float step
      const options: RangeQueryOptions = { range, intervalMs: 2000 };
      expect(Number.isInteger(ds.createRangeQuery(query, options).step)).toBeTruthy();
    });
  });

  describe('annotationQuery', () => {
    it('should transform the loki data to annotation response', async () => {
      const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
      backendSrvMock.datasourceRequest = jest
        .fn()
        .mockReturnValueOnce(
          Promise.resolve({
            data: [],
            status: 404,
          })
        )
        .mockReturnValueOnce(
          Promise.resolve({
            data: {
              streams: [
                {
                  entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
                  labels: '{label="value"}',
                },
                {
                  entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }],
                  labels: '{label2="value2"}',
                },
              ],
            },
          })
        );
      const query = makeAnnotationQueryRequest();

      const res = await ds.annotationQuery(query);
      expect(res.length).toBe(2);
      expect(res[0].text).toBe('hello');
      expect(res[0].tags).toEqual(['value']);

      expect(res[1].text).toBe('hello 2');
      expect(res[1].tags).toEqual(['value2']);
    });
  });

  describe('metricFindQuery', () => {
    const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
    const mocks = makeMetadataAndVersionsMocks();

    mocks.forEach((mock, index) => {
      it(`should return label names for Loki v${index}`, async () => {
        ds.getVersion = mock.getVersion;
        ds.metadataRequest = mock.metadataRequest;
        const query = 'label_names()';
        const res = await ds.metricFindQuery(query);
        expect(res[0].text).toEqual('label1');
        expect(res[1].text).toEqual('label2');
        expect(res.length).toBe(2);
      });
    });

    mocks.forEach((mock, index) => {
      it(`should return label names for Loki v${index}`, async () => {
        ds.getVersion = mock.getVersion;
        ds.metadataRequest = mock.metadataRequest;
        const query = 'label_names()';
        const res = await ds.metricFindQuery(query);
        expect(res[0].text).toEqual('label1');
        expect(res[1].text).toEqual('label2');
        expect(res.length).toBe(2);
      });
    });

    mocks.forEach((mock, index) => {
      it(`should return label values for Loki v${index}`, async () => {
        ds.getVersion = mock.getVersion;
        ds.metadataRequest = mock.metadataRequest;
        const query = 'label_values(label1)';
        const res = await ds.metricFindQuery(query);
        expect(res[0].text).toEqual('value1');
        expect(res[1].text).toEqual('value2');
        expect(res.length).toBe(2);
      });
    });

    mocks.forEach((mock, index) => {
      it(`should return empty array when incorrect query for Loki v${index}`, async () => {
        ds.getVersion = mock.getVersion;
        ds.metadataRequest = mock.metadataRequest;
        const query = 'incorrect_query';
        const res = await ds.metricFindQuery(query);
        expect(res.length).toBe(0);
      });
    });
  });
});

type LimitTestArgs = {
  maxDataPoints?: number;
  maxLines?: number;
  expectedLimit: number;
};
function makeLimitTest(
  instanceSettings: any,
  backendSrvMock: any,
  backendSrv: any,
  templateSrvMock: any,
  testResp: any
) {
  return ({ maxDataPoints, maxLines, expectedLimit }: LimitTestArgs) => {
    let settings = instanceSettings;
    if (Number.isFinite(maxLines)) {
      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
      settings = { ...instanceSettings, jsonData: customData };
    }
    const ds = new LokiDatasource(settings, backendSrv, templateSrvMock);
    backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));

    const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
    if (Number.isFinite(maxDataPoints)) {
      options.maxDataPoints = maxDataPoints;
    } else {
      // By default is 500
      delete options.maxDataPoints;
    }

    ds.query(options);

    expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
    expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`);
  };
}

function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> {
  const timeRange = {
    from: dateTime(),
    to: dateTime(),
  };
  return {
    annotation: {
      expr: '{test=test}',
      refId: '',
      datasource: 'loki',
      enable: true,
      name: 'test-annotation',
    },
    dashboard: {
      id: 1,
    } as any,
    range: {
      ...timeRange,
      raw: timeRange,
    },
    rangeRaw: timeRange,
  };
}

function makeMetadataAndVersionsMocks() {
  const mocks = [];
  for (let i = 0; i <= 1; i++) {
    const mock: LokiDatasource = makeMockLokiDatasource({ label1: ['value1', 'value2'], label2: ['value3', 'value4'] });
    mock.getVersion = jest.fn().mockReturnValue(`v${i}`);
    mocks.push(mock);
  }
  return mocks;
}
