import { FieldType, GrafanaConfig, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data';
import { setTemplateSrv } from '@grafana/runtime';
import { DashboardLink } from '@grafana/schema';
import { ContextSrv } from 'app/core/services/context_srv';
import { getTimeSrv, setTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TimeModel } from 'app/features/dashboard/state/TimeModel';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { variableAdapters } from 'app/features/variables/adapters';
import { createQueryVariableAdapter } from 'app/features/variables/query/adapter';

import { initTemplateSrv } from '../../../../../test/helpers/initTemplateSrv';
import { updateConfig } from '../../../../core/config';
import { getDataFrameVars, LinkSrv } from '../link_srv';

jest.mock('app/core/core', () => ({
  appEvents: {
    subscribe: () => {},
  },
}));

describe('linkSrv', () => {
  let linkSrv: LinkSrv;
  let templateSrv: TemplateSrv;
  let originalTimeService: TimeSrv;

  function initLinkSrv() {
    const _dashboard = {
      time: { from: 'now-6h', to: 'now' },
      getTimezone: jest.fn(() => 'browser'),
      timeRangeUpdated: () => {},
    } as unknown as TimeModel;

    const timeSrv = new TimeSrv({} as ContextSrv);
    timeSrv.init(_dashboard);
    timeSrv.setTime({ from: 'now-1h', to: 'now' });
    _dashboard.refresh = undefined;
    setTimeSrv(timeSrv);

    templateSrv = initTemplateSrv('key', [
      { type: 'query', name: 'home', current: { value: '127.0.0.1' } },
      { type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
    ]);

    setTemplateSrv(templateSrv);

    linkSrv = new LinkSrv();
  }

  beforeAll(() => {
    originalTimeService = getTimeSrv();
    variableAdapters.register(createQueryVariableAdapter());
  });

  beforeEach(() => {
    initLinkSrv();

    jest.resetAllMocks();
  });

  afterAll(() => {
    setTimeSrv(originalTimeService);
  });

  describe('getDataLinkUIModel', () => {
    describe('built in variables', () => {
      it('should not trim white space from data links', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'White space',
              url: 'www.google.com?query=some query',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=some query');
      });

      it('should remove new lines from data link', () => {
        expect(
          linkSrv.getDataLinkUIModel(
            {
              title: 'New line',
              url: 'www.google.com?query=some\nquery',
            },
            (v) => v,
            {}
          ).href
        ).toEqual('www.google.com?query=somequery');
      });
    });

    describe('sanitization', () => {
      const url = "javascript:alert('broken!);";
      it.each`
        disableSanitizeHtml | expected
        ${true}             | ${url}
        ${false}            | ${'about:blank'}
      `(
        "when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
        ({ disableSanitizeHtml, expected }) => {
          updateConfig({
            disableSanitizeHtml,
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });

    describe('Building links with root_url set', () => {
      it.each`
        url                 | appSubUrl     | expected
        ${'/d/XXX'}         | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${'/grafana'} | ${'d/whatever'}
        ${'/d/XXX'}         | ${''}         | ${'/d/XXX'}
        ${'/grafana/d/XXX'} | ${''}         | ${'/grafana/d/XXX'}
        ${'d/whatever'}     | ${''}         | ${'d/whatever'}
      `(
        "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
        ({ url, appSubUrl, expected }) => {
          locationUtil.initialize({
            config: { appSubUrl } as GrafanaConfig,
            getVariablesUrlParams: jest.fn(),
            getTimeRangeForUrl: jest.fn(),
          });

          const link = linkSrv.getDataLinkUIModel(
            {
              title: 'Any title',
              url,
            },
            (v) => v,
            {}
          ).href;

          expect(link).toBe(expected);
        }
      );
    });
  });

  describe('getAnchorInfo', () => {
    it('returns variable values for variable names in link.href and link.tooltip', () => {
      jest.spyOn(linkSrv, 'getLinkUrl');
      jest.spyOn(templateSrv, 'replace');

      expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
      expect(templateSrv.replace).toBeCalledTimes(0);

      const link = linkSrv.getAnchorInfo({
        type: 'link',
        icon: 'dashboard',
        tags: [],
        url: '/graph?home=$home',
        title: 'Visit home',
        tooltip: 'Visit ${home:raw}',
      } as unknown as DashboardLink);

      expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
      expect(templateSrv.replace).toBeCalledTimes(3);
      expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
    });
  });

  describe('getLinkUrl', () => {
    it('converts link urls', () => {
      const linkUrl = linkSrv.getLinkUrl({
        url: '/graph',
        asDropdown: false,
        icon: 'external link',
        targetBlank: false,
        includeVars: false,
        keepTime: false,
        tags: [],
        title: 'Visit home',
        tooltip: 'Visit home',
        type: 'link',
      });
      const linkUrlWithVar = linkSrv.getLinkUrl({
        url: '/graph?home=$home',
        asDropdown: false,
        icon: 'external link',
        targetBlank: false,
        includeVars: false,
        keepTime: false,
        tags: [],
        title: 'Visit home',
        tooltip: 'Visit home',
        type: 'link',
      });

      expect(linkUrl).toBe('/graph');
      expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
    });

    it('appends current dashboard time range if keepTime is true', () => {
      const anchorInfoKeepTime = linkSrv.getLinkUrl({
        url: '/graph',
        asDropdown: false,
        icon: 'external link',
        targetBlank: false,
        includeVars: false,
        keepTime: true,
        tags: [],
        title: 'Visit home',
        tooltip: 'Visit home',
        type: 'link',
      });

      expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
    });

    it('adds all variables to the url if includeVars is true', () => {
      const anchorInfoIncludeVars = linkSrv.getLinkUrl({
        url: '/graph',
        asDropdown: false,
        icon: 'external link',
        targetBlank: false,
        includeVars: true,
        keepTime: false,
        tags: [],
        title: 'Visit home',
        tooltip: 'Visit home',
        type: 'link',
      });

      expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
    });

    it('respects config disableSanitizeHtml', () => {
      const anchorInfo: DashboardLink = {
        url: 'javascript:alert(document.domain)',
        asDropdown: false,
        icon: 'external link',
        targetBlank: false,
        includeVars: false,
        keepTime: false,
        tags: [],
        title: 'Visit home',
        tooltip: 'Visit home',
        type: 'link',
      };

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');

      updateConfig({
        disableSanitizeHtml: true,
      });

      expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
    });
  });
});

describe('getDataFrameVars', () => {
  describe('when called with a DataFrame that contains fields without nested path', () => {
    it('then it should return correct suggestions', () => {
      const frame = toDataFrame({
        name: 'indoor',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields.temperature',
          label: 'temperature',
          documentation: `Formatted value for temperature on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields.temperature.text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  });

  describe('when called with a DataFrame that contains fields with nested path', () => {
    it('then it should return correct suggestions', () => {
      const frame = toDataFrame({
        name: 'temperatures',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields["temperature.indoor"]',
          label: 'temperature.indoor',
          documentation: `Formatted value for temperature.indoor on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["temperature.indoor"].numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["temperature.indoor"].text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  });

  describe('when called with a DataFrame that contains fields with displayName', () => {
    it('then it should return correct suggestions', () => {
      const frame = toDataFrame({
        name: 'temperatures',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields["Indoor Temperature"]',
          label: 'Indoor Temperature',
          documentation: `Formatted value for Indoor Temperature on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"].numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"].text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"]`,
          label: `Select by title`,
          documentation: `Use the title to pick the field`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  });

  describe('when called with a DataFrame that contains fields with duplicate names', () => {
    it('then it should ignore duplicates', () => {
      const frame = toDataFrame({
        name: 'temperatures',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
          { name: 'temperature.outdoor', type: FieldType.number, values: [20, 21, 22] },
        ],
      });

      frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };
      // Someone makes a mistake when renaming a field
      frame.fields[2].config = { ...frame.fields[2].config, displayName: 'Indoor Temperature' };

      const suggestions = getDataFrameVars([frame]);

      expect(suggestions).toEqual([
        {
          value: '__data.fields.time',
          label: 'time',
          documentation: `Formatted value for time on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: '__data.fields["Indoor Temperature"]',
          label: 'Indoor Temperature',
          documentation: `Formatted value for Indoor Temperature on the same row`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields[0]`,
          label: `Select by index`,
          documentation: `Enter the field order`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"].numeric`,
          label: `Show numeric value`,
          documentation: `the numeric field value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"].text`,
          label: `Show text value`,
          documentation: `the text value`,
          origin: VariableOrigin.Fields,
        },
        {
          value: `__data.fields["Indoor Temperature"]`,
          label: `Select by title`,
          documentation: `Use the title to pick the field`,
          origin: VariableOrigin.Fields,
        },
      ]);
    });
  });

  describe('when called with multiple DataFrames', () => {
    it('it should not return any suggestions', () => {
      const frame1 = toDataFrame({
        name: 'server1',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'value', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const frame2 = toDataFrame({
        name: 'server2',
        fields: [
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
          { name: 'value', type: FieldType.number, values: [10, 11, 12] },
        ],
      });

      const suggestions = getDataFrameVars([frame1, frame2]);

      expect(suggestions).toEqual([]);
    });
  });
});
