import {
  CoreApp,
  DataFrame,
  DataLink,
  DataLinkConfigOrigin,
  dateTime,
  Field,
  FieldType,
  InterpolateFunction,
  SupportedTransformationType,
  TimeRange,
  toDataFrame,
} from '@grafana/data';
import { setTemplateSrv, reportInteraction } from '@grafana/runtime';

import { initTemplateSrv } from '../../../../test/helpers/initTemplateSrv';
import { ContextSrv, setContextSrv } from '../../../core/services/context_srv';
import { setLinkSrv } from '../../panel/panellinks/link_srv';

import { getFieldLinksForExplore, getVariableUsageInfo } from './links';

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  reportInteraction: jest.fn(),
}));

describe('explore links utils', () => {
  describe('getFieldLinksForExplore', () => {
    beforeEach(() => {
      setTemplateSrv(
        initTemplateSrv('key', [
          { type: 'custom', name: 'emptyVar', current: { value: null } },
          { type: 'custom', name: 'num', current: { value: 1 } },
          { type: 'custom', name: 'test', current: { value: 'foo' } },
        ])
      );

      jest.spyOn(window, 'open').mockImplementation();
    });

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

    it('returns correct link model for external link', () => {
      const { field, range } = setup({
        title: 'external',
        url: 'http://regionalhost',
      });
      const links = getFieldLinksForExplore({
        field,
        rowIndex: ROW_WITH_TEXT_VALUE.index,
        splitOpenFn: jest.fn(),
        range,
      });

      expect(links[0].href).toBe('http://regionalhost');
      expect(links[0].title).toBe('external');
      expect(links[0].onClick).not.toBeDefined();
    });

    it('returns generates title for external link', () => {
      const { field, range } = setup({
        title: '',
        url: 'http://regionalhost',
      });
      const links = getFieldLinksForExplore({
        field,
        rowIndex: ROW_WITH_TEXT_VALUE.index,
        splitOpenFn: jest.fn(),
        range,
      });

      expect(links[0].href).toBe('http://regionalhost');
      expect(links[0].title).toBe('regionalhost');
    });

    it('returns correct link model for internal link', () => {
      const { field, range } = setup({
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          panelsState: {
            trace: {
              spanId: 'abcdef',
            },
          },
        },
      });
      const splitfn = jest.fn();
      const links = getFieldLinksForExplore({
        field,
        rowIndex: ROW_WITH_TEXT_VALUE.index,
        splitOpenFn: splitfn,
        range,
      });

      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1"}],"panelsState":{"trace":{"spanId":"abcdef"}}}'
        )}`
      );
      expect(links[0].title).toBe('test_ds');

      const preventDefault = jest.fn();

      if (links[0].onClick) {
        links[0].onClick({
          preventDefault,
        });
      }

      expect(splitfn).toBeCalledWith({
        datasourceUid: 'uid_1',
        queries: [{ query: 'query_1' }],
        range,
        panelsState: {
          trace: {
            spanId: 'abcdef',
          },
        },
      });

      expect(preventDefault).toBeCalled();

      expect(reportInteraction).toBeCalledWith('grafana_data_link_clicked', {
        app: CoreApp.Explore,
        internal: true,
        origin: DataLinkConfigOrigin.Datasource,
      });
    });

    it('returns correct link model for external link when user does not have access to explore', () => {
      const { field, range } = setup(
        {
          title: 'external',
          url: 'http://regionalhost',
        },
        false
      );
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range });

      expect(links[0].href).toBe('http://regionalhost');
      expect(links[0].title).toBe('external');
    });

    it('returns no internal links if when user does not have access to explore', () => {
      const { field, range } = setup(
        {
          title: '',
          url: '',
          internal: {
            query: { query: 'query_1' },
            datasourceUid: 'uid_1',
            datasourceName: 'test_ds',
          },
        },
        false
      );
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range });
      expect(links).toHaveLength(0);
    });

    it('returns internal links when target contains __data template variables', () => {
      const { field, range, dataFrame } = setup({
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${__data.fields.flux-dimensions}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      });
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(1);
      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
        )}`
      );
    });

    it('returns internal links when target contains targetField template variable', () => {
      const { field, range, dataFrame } = setup({
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${__targetField}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      });
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(1);
      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
        )}`
      );
    });

    it('returns internal links when target contains field name template variable', () => {
      // field cannot be hyphenated, change field name to non-hyphenated
      const noHyphenLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${fluxDimensions}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      };
      const { field, range, dataFrame } = setup(noHyphenLink, true, {
        name: 'fluxDimensions',
        type: FieldType.string,
        values: [ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value],
        config: {
          links: [noHyphenLink],
        },
      });
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(1);
      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
        )}`
      );
    });

    it('returns internal links when target contains other field name template variables', () => {
      // field cannot be hyphenated, change field name to non-hyphenated
      const noHyphenLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${fluxDimensions}-${fluxDimension2}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      };
      const { field, range, dataFrame } = setup(
        noHyphenLink,
        true,
        {
          name: 'fluxDimensions',
          type: FieldType.string,
          values: [ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value],
          config: {
            links: [noHyphenLink],
          },
        },
        [
          {
            name: 'fluxDimension2',
            type: FieldType.string,
            values: ['foo2', ROW_WITH_NULL_VALUE.value],
            config: {
              links: [noHyphenLink],
            },
          },
        ]
      );
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(1);
      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}]}'
        )}`
      );
    });

    it('returns internal links with logfmt and regex transformation', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        origin: DataLinkConfigOrigin.Correlations,
        internal: {
          query: { query: 'http_requests{app=${application} env=${environment}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [
            { type: SupportedTransformationType.Logfmt },
            { type: SupportedTransformationType.Regex, expression: 'host=(dev|prod)', mapValue: 'environment' },
          ],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['application=foo host=dev-001', 'application=bar host=prod-003'],
        config: {
          links: [transformationLink],
        },
      });

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=foo env=dev}"}]}'
        )}`
      );

      if (links[0][0].onClick) {
        links[0][0].onClick({});
      }

      expect(reportInteraction).not.toBeCalled();

      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=bar env=prod}"}]}'
        )}`
      );
    });

    it('returns internal links with 2 unnamed regex transformations and use the last transformation', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{env=${msg}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [
            { type: SupportedTransformationType.Regex, expression: 'fieldA=(asparagus|broccoli)' },
            { type: SupportedTransformationType.Regex, expression: 'fieldB=(apple|banana)' },
          ],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['fieldA=asparagus fieldB=banana', 'fieldA=broccoli fieldB=apple'],
        config: {
          links: [transformationLink],
        },
      });

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=banana}"}]}'
        )}`
      );
      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=apple}"}]}'
        )}`
      );
    });

    it('returns internal links within a result consistent with trace data', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{env=${msg}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [
            {
              type: SupportedTransformationType.Regex,
              expression: '{(?=[^\\}]*\\bkey":"keyA")[^\\}]*\\bvalue":"(.*?)".*}',
              field: 'serviceTags',
              mapValue: 'msg',
            },
          ],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'serviceTags',
        type: FieldType.other,
        values: [
          [
            { value: 'broccoli', key: 'keyA' },
            { value: 'apple', key: 'keyB' },
          ],
          [
            { key: 'keyA', value: 'cauliflower' },
            { value: 'durian', key: 'keyB' },
          ],
        ],
        config: {
          links: [transformationLink],
        },
      });

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=broccoli}"}]}'
        )}`
      );
      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=cauliflower}"}]}'
        )}`
      );
    });

    it('returns internal links with logfmt with stringified booleans', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{app=${application} isOnline=${online}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [{ type: SupportedTransformationType.Logfmt }],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['application=foo online=true', 'application=bar online=false'],
        config: {
          links: [transformationLink],
        },
      });

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=foo isOnline=true}"}]}'
        )}`
      );
      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=bar isOnline=false}"}]}'
        )}`
      );
    });

    it('returns internal links with logfmt with correct data on transformation-defined field', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{app=${application}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [{ type: SupportedTransformationType.Logfmt, field: 'fieldNamedInTransformation' }],
        },
      };

      // fieldWithLink has the transformation, but the transformation has defined fieldNamedInTransformation as its field to transform
      const { field, range, dataFrame } = setup(
        transformationLink,
        true,
        {
          name: 'fieldWithLink',
          type: FieldType.string,
          values: ['application=link', 'application=link2'],
          config: {
            links: [transformationLink],
          },
        },
        [
          {
            name: 'fieldNamedInTransformation',
            type: FieldType.string,
            values: ['application=transform', 'application=transform2'],
            config: {},
          },
        ]
      );

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=transform}"}]}'
        )}`
      );
      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=transform2}"}]}'
        )}`
      );
    });

    it('returns internal links with regex named capture groups', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{app=${application} env=${environment}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [
            {
              type: SupportedTransformationType.Regex,
              expression: '(?=.*(?<application>(grafana|loki)))(?=.*(?<environment>(dev|prod)))',
            },
          ],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['foo loki prod', 'dev bar grafana', 'prod grafana foo'],
        config: {
          links: [transformationLink],
        },
      });

      const links = [
        getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }),
        getFieldLinksForExplore({ field, rowIndex: 2, range, dataFrame }),
      ];
      expect(links[0]).toHaveLength(1);
      expect(links[0][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=loki env=prod}"}]}'
        )}`
      );
      expect(links[1]).toHaveLength(1);
      expect(links[1][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=grafana env=dev}"}]}'
        )}`
      );

      expect(links[2]).toHaveLength(1);
      expect(links[2][0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=grafana env=prod}"}]}'
        )}`
      );
    });

    it('returns internal links for non-existing fields accessed with __data.fields', () => {
      const { field, range, dataFrame } = setup({
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${__data.fields.flux-dimensions}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      });
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(1);
    });

    it('returns no internal links when target contains empty template variables', () => {
      const { field, range, dataFrame } = setup({
        title: '',
        url: '',
        internal: {
          query: { query: 'query_1-${mementoMori}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
        },
      });
      const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame });
      expect(links).toHaveLength(0);
    });

    it('does not return internal links when not all query variables are matched', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{app=${application} env=${diffVar}}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [{ type: SupportedTransformationType.Logfmt }],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['application=foo host=dev-001'],
        config: {
          links: [transformationLink],
        },
      });

      const links = [getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame })];
      expect(links[0]).toHaveLength(0);
    });

    it('does return internal link when there are no variables (static link)', () => {
      const transformationLink: DataLink = {
        title: '',
        url: '',
        internal: {
          query: { query: 'http_requests{app=test}' },
          datasourceUid: 'uid_1',
          datasourceName: 'test_ds',
          transformations: [{ type: SupportedTransformationType.Logfmt }],
        },
      };

      const { field, range, dataFrame } = setup(transformationLink, true, {
        name: 'msg',
        type: FieldType.string,
        values: ['application=foo host=dev-001'],
        config: {
          links: [transformationLink],
        },
      });

      const links = getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame });
      expect(links).toHaveLength(1);
      expect(links[0].variables?.length).toBe(1);
      expect(links[0].variables![0].variableName).toBe('msg');
      expect(links[0].variables![0].value).toBe('');
      expect(links[0].href).toBe(
        `/explore?left=${encodeURIComponent(
          '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=test}"}]}'
        )}`
      );
    });
  });

  describe('getVariableUsageInfo', () => {
    function makeDataLinkWithQuery(query: string): DataLink {
      return {
        url: '',
        title: '',
        internal: {
          datasourceUid: 'uid',
          datasourceName: 'dsName',
          query: { query },
        },
      };
    }

    function allVariablesDefinedInQuery(query: string) {
      const scopedVars = {
        testVal: { text: '', value: 'val1' },
      };
      return getVariableUsageInfo(makeDataLinkWithQuery(query), scopedVars).allVariablesDefined;
    }

    it('returns true when query contains variables and all variables are used', () => {
      expect(allVariablesDefinedInQuery('test ${testVal}')).toBe(true);
    });

    it('ignores global variables', () => {
      expect(allVariablesDefinedInQuery('test ${__rate_interval} $__from $__to')).toBe(true);
    });

    it('returns false when query contains variables and no variables are used', () => {
      expect(allVariablesDefinedInQuery('test ${diffVar}')).toBe(false);
    });

    it('returns false when query contains variables and some variables are used', () => {
      expect(allVariablesDefinedInQuery('test ${testVal} ${diffVar}')).toBe(false);
    });

    it('returns true when query contains no variables', () => {
      expect(allVariablesDefinedInQuery('test')).toBe(true);
    });

    it('returns deduplicated list of variables', () => {
      const dataLink = makeDataLinkWithQuery('test ${test} ${foo} ${test:raw} $test');
      const scopedVars = {
        testVal: { text: '', value: 'val1' },
      };
      const variables = getVariableUsageInfo(dataLink, scopedVars).variables;
      expect(variables).toHaveLength(2);
    });
  });
});

const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 };
const ROW_WITH_NULL_VALUE = { value: null, index: 1 };

function setup(
  link: DataLink,
  hasAccess = true,
  fieldOverride?: Field<string | Array<{ key: string; value: string }> | null>, // key/value array for traceView fields
  dataFrameOtherFieldOverride?: Field[]
) {
  setLinkSrv({
    getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin) {
      return {
        href: link.url,
        title: link.title,
        target: '_blank',
        origin: origin,
      };
    },
    getAnchorInfo(link) {
      return { ...link };
    },
    getLinkUrl(link) {
      return link.url;
    },
  });

  setContextSrv({
    hasAccessToExplore: () => hasAccess,
  } as ContextSrv);

  const field: Field<string | null> = {
    name: 'flux-dimensions',
    type: FieldType.string,
    values: [ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value],
    config: {
      links: [link],
    },
  };

  let fieldsArr = [fieldOverride || field];

  if (dataFrameOtherFieldOverride) {
    fieldsArr = [...fieldsArr, ...dataFrameOtherFieldOverride];
  }

  const dataFrame: DataFrame = toDataFrame({
    fields: fieldsArr,
  });

  const range: TimeRange = {
    from: dateTime('2020-10-14T00:00:00'),
    to: dateTime('2020-10-14T01:00:00'),
    raw: {
      from: 'now-1h',
      to: 'now',
    },
  };

  return { range, field: fieldOverride || field, dataFrame };
}
