import { DataSourceApi } from '@grafana/data';
import { config, setTemplateSrv, TemplateSrv } from '@grafana/runtime';
import {
  CustomVariable,
  ConstantVariable,
  IntervalVariable,
  QueryVariable,
  DataSourceVariable,
  AdHocFiltersVariable,
  GroupByVariable,
  TextBoxVariable,
  SceneVariableSet,
} from '@grafana/scenes';
import { DataQuery, DataSourceJsonData, VariableHide, VariableType } from '@grafana/schema';
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';

import { AdHocFiltersVariableEditor } from './editors/AdHocFiltersVariableEditor';
import { ConstantVariableEditor } from './editors/ConstantVariableEditor';
import { CustomVariableEditor } from './editors/CustomVariableEditor';
import { DataSourceVariableEditor } from './editors/DataSourceVariableEditor';
import { GroupByVariableEditor } from './editors/GroupByVariableEditor';
import { IntervalVariableEditor } from './editors/IntervalVariableEditor';
import { QueryVariableEditor } from './editors/QueryVariableEditor';
import { TextBoxVariableEditor } from './editors/TextBoxVariableEditor';
import {
  isEditableVariableType,
  EDITABLE_VARIABLES,
  EDITABLE_VARIABLES_SELECT_ORDER,
  getVariableTypeSelectOptions,
  getVariableEditor,
  getVariableScene,
  hasVariableOptions,
  EditableVariableType,
  getDefinition,
  getOptionDataSourceTypes,
  getNextAvailableId,
  getVariableDefault,
  isSceneVariableInstance,
} from './utils';

const templateSrv = {
  getAdhocFilters: jest.fn().mockReturnValue([{ key: 'origKey', operator: '=', value: '' }]),
} as unknown as TemplateSrv;

const dsMock: DataSourceApi = {
  meta: {
    id: DASHBOARD_DATASOURCE_PLUGIN_ID,
  },
  name: SHARED_DASHBOARD_QUERY,
  type: SHARED_DASHBOARD_QUERY,
  uid: SHARED_DASHBOARD_QUERY,
  getRef: () => {
    return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY };
  },
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  getDataSourceSrv: () => ({
    get: async () => dsMock,
    getList: () => {
      return [
        {
          name: 'DataSourceInstance1',
          uid: 'ds1',
          meta: {
            name: 'ds1',
            id: 'dsTestDataSource',
          },
        },
      ];
    },
  }),
}));

describe('isEditableVariableType', () => {
  it('should return true for editable variable types', () => {
    const editableTypes: VariableType[] = [
      'custom',
      'query',
      'constant',
      'interval',
      'datasource',
      'adhoc',
      'groupby',
      'textbox',
    ];
    editableTypes.forEach((type) => {
      expect(isEditableVariableType(type)).toBe(true);
    });
  });

  it('should return false for non-editable variable types', () => {
    const nonEditableTypes: VariableType[] = ['system'];
    nonEditableTypes.forEach((type) => {
      expect(isEditableVariableType(type)).toBe(false);
    });
  });
});

describe('isSceneVariableInstance', () => {
  it.each([
    CustomVariable,
    QueryVariable,
    ConstantVariable,
    IntervalVariable,
    DataSourceVariable,
    AdHocFiltersVariable,
    GroupByVariable,
    TextBoxVariable,
  ])('should return true for scene variable instances %s', (instanceType) => {
    const variable = new instanceType({ name: 'MyVariable' });
    expect(isSceneVariableInstance(variable)).toBe(true);
  });

  it('should return false for non-scene variable instances', () => {
    const variable = {
      name: 'MyVariable',
      type: 'query',
    };
    expect(variable).not.toBeInstanceOf(QueryVariable);
  });
});

describe('getVariableTypeSelectOptions', () => {
  describe('when groupByVariable is enabled', () => {
    beforeAll(() => {
      config.featureToggles.groupByVariable = true;
    });

    afterAll(() => {
      config.featureToggles.groupByVariable = false;
    });

    it('should contain all editable variable types', () => {
      const options = getVariableTypeSelectOptions();
      expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length);

      EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => {
        expect(EDITABLE_VARIABLES).toHaveProperty(type);
      });
    });

    it('should return an array of selectable values for editable variable types', () => {
      const options = getVariableTypeSelectOptions();
      expect(options).toHaveLength(8);

      options.forEach((option, index) => {
        const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index];
        const variableTypeConfig = EDITABLE_VARIABLES[editableType];

        expect(option.value).toBe(editableType);
        expect(option.label).toBe(variableTypeConfig.name);
        expect(option.description).toBe(variableTypeConfig.description);
      });
    });
  });

  describe('when groupByVariable is disabled', () => {
    it('should contain all editable variable types except groupby', () => {
      const options = getVariableTypeSelectOptions();
      expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length - 1);

      EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => {
        expect(EDITABLE_VARIABLES).toHaveProperty(type);
      });
    });

    it('should return an array of selectable values for editable variable types', () => {
      const options = getVariableTypeSelectOptions();
      expect(options).toHaveLength(7);

      options.forEach((option, index) => {
        const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index];
        const variableTypeConfig = EDITABLE_VARIABLES[editableType];

        expect(option.value).toBe(editableType);
        expect(option.label).toBe(variableTypeConfig.name);
        expect(option.description).toBe(variableTypeConfig.description);
      });
    });
  });
});

describe('getVariableEditor', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
    'should define an editor for variable type "%s"',
    (type) => {
      const editor = getVariableEditor(type);
      expect(editor).toBeDefined();
    }
  );

  it.each([
    ['custom', CustomVariableEditor],
    ['query', QueryVariableEditor],
    ['constant', ConstantVariableEditor],
    ['interval', IntervalVariableEditor],
    ['datasource', DataSourceVariableEditor],
    ['adhoc', AdHocFiltersVariableEditor],
    ['groupby', GroupByVariableEditor],
    ['textbox', TextBoxVariableEditor],
  ])('should return the correct editor for variable type "%s"', (type, ExpectedVariableEditor) => {
    expect(getVariableEditor(type as EditableVariableType)).toBe(ExpectedVariableEditor);
  });
});

describe('getVariableScene', () => {
  beforeAll(() => {
    setTemplateSrv(templateSrv);
  });

  it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
    'should define a scene object for every variable type',
    (type) => {
      const variable = getVariableScene(type, { name: 'foo' });
      expect(variable).toBeDefined();
    }
  );

  it.each([
    ['custom', CustomVariable],
    ['query', QueryVariable],
    ['interval', IntervalVariable],
    ['datasource', DataSourceVariable],
    ['adhoc', AdHocFiltersVariable],
    ['groupby', GroupByVariable],
    ['textbox', TextBoxVariable],
  ])('should return the scene variable instance for the given editable variable type', (type, instanceType) => {
    const initialState = { name: 'MyVariable' };
    const sceneVariable = getVariableScene(type as EditableVariableType, initialState);
    expect(sceneVariable).toBeInstanceOf(instanceType);
    expect(sceneVariable.state.name).toBe(initialState.name);
    expect(sceneVariable.state.hide).toBe(undefined);
  });

  it('should return the scene variable instance for the constant editable variable type', () => {
    const initialState = { name: 'MyVariable' };
    const sceneVariable = getVariableScene('constant' as EditableVariableType, initialState);
    expect(sceneVariable).toBeInstanceOf(ConstantVariable);
    expect(sceneVariable.state.name).toBe(initialState.name);
    expect(sceneVariable.state.hide).toBe(VariableHide.hideVariable);
  });
});

describe('hasVariableOptions', () => {
  it('should return true for scene variables with options property', () => {
    const variableWithOptions = new CustomVariable({
      name: 'MyVariable',
      options: [{ value: 'option1', label: 'Option 1' }],
    });
    expect(hasVariableOptions(variableWithOptions)).toBe(true);
  });

  it('should return false for scene variables without options property', () => {
    const variableWithoutOptions = new ConstantVariable({ name: 'MyVariable' });
    expect(hasVariableOptions(variableWithoutOptions)).toBe(false);
  });
});

describe('getDefinition', () => {
  it('returns the correct definition for QueryVariable when definition is defined', () => {
    const model = new QueryVariable({
      name: 'custom0',
      query: '',
      definition: 'legacy ABC query definition',
    });
    expect(getDefinition(model)).toBe('legacy ABC query definition');
  });

  it('returns the correct definition for QueryVariable when definition is not defined', () => {
    const model = new QueryVariable({
      name: 'custom0',
      query: 'ABC query',
      definition: '',
    });
    expect(getDefinition(model)).toBe('ABC query');
  });

  it('returns the correct definition for DataSourceVariable', () => {
    const model = new DataSourceVariable({
      name: 'ds0',
      pluginId: 'datasource-plugin',
      value: 'datasource-value',
    });
    expect(getDefinition(model)).toBe('datasource-plugin');
  });

  it('returns the correct definition for CustomVariable', () => {
    const model = new CustomVariable({
      name: 'custom0',
      query: 'Custom, A, B, C',
    });
    expect(getDefinition(model)).toBe('Custom, A, B, C');
  });

  it('returns the correct definition for IntervalVariable', () => {
    const model = new IntervalVariable({
      name: 'interval0',
      intervals: ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'],
    });
    expect(getDefinition(model)).toBe('1m,5m,15m,30m,1h,6h,12h,1d');
  });

  it('returns the correct definition for TextBoxVariable', () => {
    const model = new TextBoxVariable({
      name: 'textbox0',
      value: 'TextBox Value',
    });
    expect(getDefinition(model)).toBe('TextBox Value');
  });

  it('returns the correct definition for ConstantVariable', () => {
    const model = new ConstantVariable({
      name: 'constant0',
      value: 'Constant Value',
    });
    expect(getDefinition(model)).toBe('Constant Value');
  });
});

describe('getOptionDataSourceTypes', () => {
  it('should return all data source types when no data source types are specified', () => {
    const optionTypes = getOptionDataSourceTypes();
    expect(optionTypes).toHaveLength(2);
    // in the old code we always had an empty option
    expect(optionTypes[0].value).toBe('');
    expect(optionTypes[1].label).toBe('ds1');
  });
});

describe('getNextAvailableId', () => {
  it('should return the initial ID for an empty array', () => {
    const sceneVariables = new SceneVariableSet({
      variables: [],
    });

    expect(getNextAvailableId('query', sceneVariables.state.variables)).toBe('query0');
  });

  it('should return a non-conflicting ID for a non-empty array', () => {
    const variable = new QueryVariable({
      name: 'query0',
      label: 'test-label',
      description: 'test-desc',
      value: ['selected-value'],
      text: ['selected-value-text'],
      datasource: { uid: 'fake-std', type: 'fake-std' },
      query: 'query',
      includeAll: true,
      allValue: 'test-all',
      isMulti: true,
    });

    const sceneVariables = new SceneVariableSet({
      variables: [variable],
    });

    expect(getNextAvailableId('query', sceneVariables.state.variables)).toBe('query1');
  });
});

describe('getVariableDefault', () => {
  it('should return a QueryVariable instance with the correct name', () => {
    const sceneVariables = new SceneVariableSet({
      variables: [],
    });

    const defaultVariable = getVariableDefault(sceneVariables.state.variables);

    expect(defaultVariable).toBeInstanceOf(QueryVariable);
    expect(defaultVariable.state.name).toBe('query0');
  });
});
