// Copyright 2021 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { analyzeCompletion, computeStartCompletePosition, computeEndCompletePosition, ContextKind, durationWithUnitRegexp } from './hybrid';
import { createEditorState, mockedMetricsTerms, mockPrometheusServer } from '../test/utils-test';
import { Completion, CompletionContext } from '@codemirror/autocomplete';
import {
  aggregateOpModifierTerms,
  aggregateOpTerms,
  atModifierTerms,
  binOpModifierTerms,
  binOpTerms,
  durationTerms,
  functionIdentifierTerms,
  matchOpTerms,
  numberTerms,
  snippets,
} from './promql.terms';
import { EqlSingle, Neq } from '@prometheus-io/lezer-promql';
import { syntaxTree } from '@codemirror/language';
import { newCompleteStrategy } from './index';
import nock from 'nock';

describe('analyzeCompletion test', () => {
  const testCases = [
    {
      title: 'empty expr',
      expr: '',
      pos: 0,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: '',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.Number },
      ],
    },
    {
      title: 'simple metric & number autocompletion',
      expr: 'go_',
      pos: 3, // cursor is at the end of the expr
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: 'go_',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.Number },
      ],
    },
    {
      title: 'metric/function/aggregation autocompletion',
      expr: 'sum()',
      pos: 4,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: '',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
      ],
    },
    {
      title: 'autocomplete AggregateOpModifier or BinOp after closing aggregation',
      expr: 'sum()',
      pos: 5, // cursor is after the closing bracket
      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
    },
    {
      title: 'metric/function/aggregation autocompletion in incomplete function',
      expr: 'sum(',
      pos: 4,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: '',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
      ],
    },
    {
      title: 'autocomplete binOp after closing function',
      expr: 'rate(foo[5m])',
      pos: 13, // cursor is after the closing bracket
      expectedContext: [{ kind: ContextKind.BinOp }],
    },
    {
      title: 'metric/function/aggregation autocompletion 2',
      expr: 'sum(rat)',
      pos: 7,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: 'rat',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
      ],
    },
    {
      title: 'metric/function/aggregation autocompletion 3',
      expr: 'sum(rate())',
      pos: 9,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: '',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
      ],
    },
    {
      title: 'autocomplete AggregateOpModifier or BinOp after closing nested aggregation',
      expr: 'sum(rate(foo[5m]))',
      pos: 18, // cursor is after the closing bracket
      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete binOp after closing aggregation with existing modifier',
      expr: 'sum by(job)(rate(foo[5m]))',
      pos: 26, // cursor is after the closing bracket
      expectedContext: [{ kind: ContextKind.BinOp }],
    },
    {
      title: 'metric/function/aggregation autocompletion 4',
      expr: 'sum(rate(my_))',
      pos: 12,
      expectedContext: [
        {
          kind: ContextKind.MetricName,
          metricName: 'my_',
        },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
      ],
    },
    {
      title: 'autocomplete binOp modifier or metric or number',
      expr: 'metric_name / ignor',
      pos: 19,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'ignor' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
      ],
    },
    {
      title: 'autocomplete binOp modifier or metric or number 2',
      expr: 'sum(http_requests_total{method="GET"} / o)',
      pos: 41,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'o' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 1',
      expr: '1 > b)',
      pos: 5,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 2',
      expr: '1 == b)',
      pos: 6,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 3',
      expr: '1 != b)',
      pos: 6,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 4',
      expr: '1 > b)',
      pos: 5,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 5',
      expr: '1 >= b)',
      pos: 6,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 6',
      expr: '1 <= b)',
      pos: 6,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'autocomplete bool/binOp modifier/metric/number 7',
      expr: '1 < b)',
      pos: 5,
      expectedContext: [
        { kind: ContextKind.MetricName, metricName: 'b' },
        { kind: ContextKind.Function },
        { kind: ContextKind.Aggregation },
        { kind: ContextKind.BinOpModifier },
        { kind: ContextKind.Number },
        { kind: ContextKind.Bool },
      ],
    },
    {
      title: 'starting to autocomplete labelName in aggregate modifier',
      expr: 'sum by ()',
      pos: 8, // cursor is between the bracket
      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
    },
    {
      title: 'starting to autocomplete labelName in aggregate modifier with metric name',
      expr: 'sum(up) by ()',
      pos: 12, // cursor is between ()
      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'up' }],
    },
    {
      title: 'starting to autocomplete labelName in aggregate modifier with metric name in front',
      expr: 'sum by ()(up)',
      pos: 8, // cursor is between ()
      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'up' }],
    },
    {
      title: 'continue to autocomplete labelName in aggregate modifier',
      expr: 'sum by (myL)',
      pos: 11, // cursor is between the bracket after the string myL
      expectedContext: [{ kind: ContextKind.LabelName }],
    },
    {
      title: 'continue to autocomplete QuotedLabelName in aggregate modifier',
      expr: 'sum by ("myL")',
      pos: 12, // cursor is between the bracket after the string myL
      expectedContext: [{ kind: ContextKind.LabelName }],
    },
    {
      title: 'autocomplete labelName in a list',
      expr: 'sum by (myLabel1,)',
      pos: 17, // cursor is between the bracket after the string myLab
      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
    },
    {
      title: 'autocomplete labelName in a list 2',
      expr: 'sum by (myLabel1, myLab)',
      pos: 23, // cursor is between the bracket after the string myLab
      expectedContext: [{ kind: ContextKind.LabelName }],
    },
    {
      title: 'autocomplete labelName in a list 2',
      expr: 'sum by ("myLabel1", "myLab")',
      pos: 27, // cursor is between the bracket after the string myLab
      expectedContext: [{ kind: ContextKind.LabelName }],
    },
    {
      title: 'autocomplete labelName associated to a metric',
      expr: 'metric_name{}',
      pos: 12, // cursor is between the bracket
      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
    },
    {
      title: 'autocomplete labelName that defined a metric',
      expr: '{}',
      pos: 1, // cursor is between the bracket
      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
    },
    {
      title: 'continue to autocomplete labelName associated to a metric',
      expr: 'metric_name{myL}',
      pos: 15, // cursor is between the bracket after the string myL
      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
    },
    {
      title: 'continue to autocomplete labelName associated to a metric 2',
      expr: 'metric_name{myLabel="labelValue",}',
      pos: 33, // cursor is between the bracket after the comma
      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
    },
    {
      title: 'no label suggestions after closing matcher',
      expr: 'up{job="prometheus"}',
      pos: 20, // cursor is right after the closing curly bracket
      expectedContext: [],
    },
    {
      title: 'continue autocomplete labelName that defined a metric',
      expr: '{myL}',
      pos: 4, // cursor is between the bracket after the string myL
      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
    },
    {
      title: 'continue autocomplete labelName that defined a metric 2',
      expr: '{myLabel="labelValue",}',
      pos: 22, // cursor is between the bracket after the comma
      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
    },
    {
      title: 'continue to autocomplete quoted labelName associated to a metric',
      expr: '{"metric_"}',
      pos: 10, // cursor is between the bracket after the string metric_
      expectedContext: [{ kind: ContextKind.MetricName, metricName: 'metric_' }],
    },
    {
      title: 'autocomplete the labelValue with metricName + labelName',
      expr: 'metric_name{labelName=""}',
      pos: 23, // cursor is between the quotes
      expectedContext: [
        {
          kind: ContextKind.LabelValue,
          metricName: 'metric_name',
          labelName: 'labelName',
          matchers: [
            {
              name: 'labelName',
              type: EqlSingle,
              value: '',
            },
          ],
        },
      ],
    },
    {
      title: 'autocomplete the labelValue with metricName + labelName 2',
      expr: 'metric_name{labelName="labelValue", labelName!=""}',
      pos: 48, // cursor is between the quotes
      expectedContext: [
        {
          kind: ContextKind.LabelValue,
          metricName: 'metric_name',
          labelName: 'labelName',
          matchers: [
            {
              name: 'labelName',
              type: EqlSingle,
              value: 'labelValue',
            },
            {
              name: 'labelName',
              type: Neq,
              value: '',
            },
          ],
        },
      ],
    },
    {
      title: 'autocomplete the labelValue with metricName + quoted labelName',
      expr: 'metric_name{labelName="labelValue", "labelName"!=""}',
      pos: 50, // cursor is between the quotes
      expectedContext: [
        {
          kind: ContextKind.LabelValue,
          metricName: 'metric_name',
          labelName: 'labelName',
          matchers: [
            {
              name: 'labelName',
              type: Neq,
              value: '',
            },
            {
              name: 'labelName',
              type: EqlSingle,
              value: 'labelValue',
            },
          ],
        },
      ],
    },
    {
      title: 'autocomplete the labelValue associated to a labelName',
      expr: '{labelName=""}',
      pos: 12, // cursor is between the quotes
      expectedContext: [
        {
          kind: ContextKind.LabelValue,
          metricName: '',
          labelName: 'labelName',
          matchers: [
            {
              name: 'labelName',
              type: EqlSingle,
              value: '',
            },
          ],
        },
      ],
    },
    {
      title: 'autocomplete the labelValue associated to a labelName 2',
      expr: '{labelName="labelValue", labelName!=""}',
      pos: 37, // cursor is between the quotes
      expectedContext: [
        {
          kind: ContextKind.LabelValue,
          metricName: '',
          labelName: 'labelName',
          matchers: [
            {
              name: 'labelName',
              type: EqlSingle,
              value: 'labelValue',
            },
            {
              name: 'labelName',
              type: Neq,
              value: '',
            },
          ],
        },
      ],
    },
    {
      title: 'autocomplete AggregateOpModifier or BinOp',
      expr: 'sum() b',
      pos: 7, // cursor is after the 'b'
      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete AggregateOpModifier or BinOp 2',
      expr: 'sum(rate(foo[5m])) an',
      pos: 21,
      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete AggregateOpModifier or BinOp or Offset',
      expr: 'sum b',
      pos: 5, // cursor is after 'b'
      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'autocomplete binOp',
      expr: 'metric_name !',
      pos: 13,
      expectedContext: [{ kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete binOp 2',
      expr: 'metric_name =',
      pos: 13,
      expectedContext: [{ kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete matchOp',
      expr: 'go{instance=""}',
      pos: 12, // cursor is after the 'equal'
      expectedContext: [{ kind: ContextKind.MatchOp }],
    },
    {
      title: 'autocomplete matchOp 2',
      expr: 'metric_name{labelName!}',
      pos: 22, // cursor is after '!'
      expectedContext: [{ kind: ContextKind.MatchOp }],
    },
    {
      title: 'autocomplete matchOp 3',
      expr: 'metric_name{"labelName"!}',
      pos: 24, // cursor is after '!'
      expectedContext: [{ kind: ContextKind.BinOp }],
    },
    {
      title: 'autocomplete duration with offset',
      expr: 'http_requests_total offset 5',
      pos: 28,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration with offset',
      expr: 'sum(http_requests_total{method="GET"} offset 4)',
      pos: 46,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete offset or binOp',
      expr: 'http_requests_total off',
      pos: 23,
      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'autocomplete offset or binOp 2',
      expr: 'metric_name unle',
      pos: 16,
      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'autocomplete offset or binOp 3',
      expr: 'http_requests_total{method="GET"} off',
      pos: 37,
      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'autocomplete offset or binOp 4',
      expr: 'rate(foo[5m]) un',
      pos: 16,
      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'autocomplete offset or binop 5',
      expr: 'sum(http_requests_total{method="GET"} off)',
      pos: 41,
      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
    },
    {
      title: 'not autocompleting duration for a matrixSelector',
      expr: 'go[]',
      pos: 3,
      expectedContext: [],
    },
    {
      title: 'not autocompleting duration for a matrixSelector 2',
      expr: 'go{l1="l2"}[]',
      pos: 12,
      expectedContext: [],
    },
    {
      title: 'autocomplete duration for a matrixSelector',
      expr: 'go[5]',
      pos: 4,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a matrixSelector 2',
      expr: 'go[5d1]',
      pos: 6,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a matrixSelector 3',
      expr: 'rate(my_metric{l1="l2"}[25])',
      pos: 26,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a matrixSelector 4',
      expr: 'rate(my_metric{l1="l2"}[25d5])',
      pos: 28,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'do not autocomplete duration when unit already present in matrixSelector',
      expr: 'rate(foo[5m])',
      pos: 10,
      expectedContext: [],
    },
    {
      title: 'do not autocomplete duration when multi char unit already present in matrixSelector',
      expr: 'rate(foo[5ms])',
      pos: 10,
      expectedContext: [],
    },
    {
      title: 'autocomplete duration for a subQuery',
      expr: 'go[5d:5]',
      pos: 7,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a subQuery 2',
      expr: 'go[5d:5d4]',
      pos: 9,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a subQuery 3',
      expr: 'rate(my_metric{l1="l2"}[25d:6])',
      pos: 29,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete duration for a subQuery 4',
      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
      pos: 31,
      expectedContext: [{ kind: ContextKind.Duration }],
    },
    {
      title: 'autocomplete at modifiers',
      expr: '1 @ s',
      pos: 5,
      expectedContext: [{ kind: ContextKind.AtModifiers }],
    },
    {
      title: 'autocomplete topk params',
      expr: 'topk()',
      pos: 5,
      expectedContext: [{ kind: ContextKind.Number }],
    },
    {
      title: 'autocomplete topk params 2',
      expr: 'topk(inf,)',
      pos: 9,
      expectedContext: [{ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
    },
    {
      title: 'autocomplete topk params 3',
      expr: 'topk(inf,r)',
      pos: 10,
      expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
    },
    {
      title: 'autocomplete topk params 4',
      expr: 'topk by(instance) ()',
      pos: 19,
      expectedContext: [{ kind: ContextKind.Number }],
    },
    {
      title: 'autocomplete topk params 5',
      expr: 'topk by(instance) (inf,r)',
      pos: 24,
      expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
    },
  ];
  testCases.forEach((value) => {
    it(value.title, () => {
      const state = createEditorState(value.expr);
      const node = syntaxTree(state).resolve(value.pos, -1);
      const result = analyzeCompletion(state, node, value.pos);
      expect(result).toEqual(value.expectedContext);
    });
  });
});

describe('durationWithUnitRegexp test', () => {
  it('should match complete durations with units', () => {
    const testCases = [
      { input: '5m', expected: true },
      { input: '30s', expected: true },
      { input: '1h', expected: true },
      { input: '500ms', expected: true },
      { input: '2d', expected: true },
      { input: '1w', expected: true },
      { input: '1y', expected: true },
      { input: '1d2h', expected: true },
      { input: '2h30m', expected: true },
      { input: '1h2m3s', expected: true },
      { input: '250ms2s', expected: true },
      { input: '2h3m4s5ms', expected: true },
      { input: '5', expected: false },
      { input: '5m5', expected: false },
      { input: 'm', expected: false },
      { input: 'd', expected: false },
      { input: '', expected: false },
      { input: '1hms', expected: false },
      { input: '2x', expected: false },
    ];
    testCases.forEach(({ input, expected }) => {
      expect(durationWithUnitRegexp.test(input)).toBe(expected);
    });
  });

  it('should not match durations without units or partial units', () => {
    const testCases = ['5', '30', '100', '5m5', 'm', 'd'];
    testCases.forEach((input) => {
      expect(durationWithUnitRegexp.test(input)).toBe(false);
    });
  });
});

describe('computeStartCompletePosition test', () => {
  const testCases = [
    {
      title: 'empty bracket',
      expr: '{}',
      pos: 1, // cursor is between the bracket
      expectedStart: 1,
    },
    {
      title: 'empty bracket 2',
      expr: 'metricName{}',
      pos: 11, // cursor is between the bracket
      expectedStart: 11,
    },
    {
      title: 'empty bracket 3',
      expr: 'sum by()',
      pos: 7, // cursor is between the bracket
      expectedStart: 7,
    },
    {
      title: 'empty bracket 4',
      expr: 'sum by(test) ()',
      pos: 14, // cursor is between the bracket
      expectedStart: 14,
    },
    {
      title: 'empty bracket 5',
      expr: 'sum()',
      pos: 4, // cursor is between the bracket
      expectedStart: 4,
    },
    {
      title: 'empty bracket 6',
      expr: 'sum(rate())',
      pos: 9, // cursor is between the bracket
      expectedStart: 9,
    },
    {
      title: 'start should be equal to the pos after closing function',
      expr: 'rate(foo[5m])',
      pos: 13, // cursor is after the closing bracket
      expectedStart: 13,
    },
    {
      title: 'start should be equal to the pos after closing aggregation',
      expr: 'sum(rate(foo[5m]))',
      pos: 18, // cursor is after the closing bracket
      expectedStart: 18,
    },
    {
      title: 'bracket containing a substring',
      expr: '{myL}',
      pos: 4, // cursor is between the bracket
      expectedStart: 1,
    },
    {
      title: 'bracket containing a substring 2',
      expr: '{myLabel="LabelValue",}',
      pos: 22, // cursor is after the comma
      expectedStart: 22,
    },
    {
      title: 'bracket containing a substring 2',
      expr: 'metricName{myL}',
      pos: 14, // cursor is between the bracket
      expectedStart: 11,
    },
    {
      title: 'bracket containing a substring 3',
      expr: 'metricName{myLabel="LabelValue",}',
      pos: 32, // cursor is after the comma
      expectedStart: 32,
    },
    {
      title: 'bracket containing a substring 4',
      expr: 'sum by(myL)',
      pos: 10, // cursor is between the bracket
      expectedStart: 7,
    },
    {
      title: 'bracket containing a substring 5',
      expr: 'sum by(myLabel,)',
      pos: 15, // cursor is after the comma
      expectedStart: 15,
    },
    {
      title: 'bracket containing a substring 6',
      expr: 'sum(ra)',
      pos: 6, // cursor is between the bracket
      expectedStart: 4,
    },
    {
      title: 'bracket containing a substring 7',
      expr: 'sum(rate(my))',
      pos: 11, // cursor is between the bracket
      expectedStart: 9,
    },
    {
      title: 'start should not be at the beginning of the substring',
      expr: 'metric_name{labelName!}',
      pos: 22,
      expectedStart: 21,
    },
    {
      title: 'start should not be at the beginning of the substring 2',
      expr: 'metric_name{labelName!="labelValue"}',
      pos: 22,
      expectedStart: 21,
    },
    {
      title: 'start should be equal to the pos for the duration of an offset',
      expr: 'http_requests_total offset 5',
      pos: 28,
      expectedStart: 28,
    },
    {
      title: 'start should be equal to the pos for the duration of an offset 2',
      expr: 'http_requests_total offset 587',
      pos: 30,
      expectedStart: 30,
    },
    {
      title: 'start should be equal to the pos for the duration of an offset 3',
      expr: 'http_requests_total offset 587',
      pos: 29,
      expectedStart: 29,
    },
    {
      title: 'start should be equal to the pos for the duration of an offset 4',
      expr: 'sum(http_requests_total{method="GET"} offset 4)',
      pos: 46,
      expectedStart: 46,
    },
    {
      title: 'start should not be equal to the pos for the duration in a matrix selector',
      expr: 'go[]',
      pos: 3,
      expectedStart: 0,
    },
    {
      title: 'start should be equal to the pos for the duration in a matrix selector',
      expr: 'go[5]',
      pos: 4,
      expectedStart: 4,
    },
    {
      title: 'start should be equal to the pos for the duration in a matrix selector 2',
      expr: 'go[5d5]',
      pos: 6,
      expectedStart: 6,
    },
    {
      title: 'start should be equal to the pos for the duration in a matrix selector 3',
      expr: 'rate(my_metric{l1="l2"}[25])',
      pos: 26,
      expectedStart: 26,
    },
    {
      title: 'start should be equal to the pos for the duration in a matrix selector 4',
      expr: 'rate(my_metric{l1="l2"}[25d5])',
      pos: 28,
      expectedStart: 28,
    },
    {
      title: 'start should be equal to the pos for the duration in a subquery selector',
      expr: 'go[5d:5]',
      pos: 7,
      expectedStart: 7,
    },
    {
      title: 'start should be equal to the pos for the duration in a subquery selector 2',
      expr: 'go[5d:5d5]',
      pos: 9,
      expectedStart: 9,
    },
    {
      title: 'start should be equal to the pos for the duration in a subquery selector 3',
      expr: 'rate(my_metric{l1="l2"}[25d:6])',
      pos: 29,
      expectedStart: 29,
    },
    {
      title: 'start should be equal to the pos for the duration in a subquery selector 3',
      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
      pos: 31,
      expectedStart: 31,
    },
  ];
  testCases.forEach((value) => {
    it(value.title, () => {
      const state = createEditorState(value.expr);
      const node = syntaxTree(state).resolve(value.pos, -1);
      const result = computeStartCompletePosition(state, node, value.pos);
      expect(result).toEqual(value.expectedStart);
    });
  });
});

describe('computeEndCompletePosition test', () => {
  const testCases = [
    {
      title: 'cursor at end of metric name',
      expr: 'metric_name',
      pos: 11, // cursor is at the end
      expectedEnd: 11,
    },
    {
      title: 'cursor in middle of metric name - should extend to end',
      expr: 'coredns_cache_hits_total',
      pos: 14, // cursor is after 'coredns_cache_' (before 'hits')
      expectedEnd: 24, // should extend to end of 'coredns_cache_hits_total'
    },
    {
      title: 'cursor in middle of metric name inside rate() - should extend to end',
      expr: 'rate(coredns_cache_hits_total[2m])',
      pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
      expectedEnd: 29, // should extend to end of 'coredns_cache_hits_total'
    },
    {
      title: 'cursor in middle of metric name inside sum(rate()) - should extend to end',
      expr: 'sum(rate(coredns_cache_hits_total[2m]))',
      pos: 24, // cursor is after 'coredns_cache_' (before 'hits')
      expectedEnd: 33, // should extend to end of 'coredns_cache_hits_total'
    },
    {
      title: 'cursor at beginning of metric name - should extend to end',
      expr: 'metric_name',
      pos: 1, // cursor after 'm'
      expectedEnd: 11,
    },
    {
      title: 'cursor in middle of incomplete function name - should extend to end',
      expr: 'sum_ov',
      pos: 4, // cursor after 'sum_' (before 'ov')
      expectedEnd: 6, // should extend to end of 'sum_ov'
    },
    {
      title: 'cursor in middle of incomplete function name within aggregator - should extend to end',
      expr: 'sum(sum_ov(foo[5m]))',
      pos: 8, // cursor after 'sum_' (before 'ov')
      expectedEnd: 10, // should extend to end of 'sum_ov'
    },
    {
      title: 'empty bracket - ends before the closing bracket',
      expr: '{}',
      pos: 1,
      expectedEnd: 1,
    },
    {
      title: 'cursor in label matchers - ends before the closing bracket',
      expr: 'metric_name{label="value"}',
      pos: 12, // cursor after '{'
      expectedEnd: 25,
    },
    {
      title: 'cursor in middle of label name in grouping clause - should extend to end',
      expr: 'sum by (instance_name)',
      pos: 12, // cursor after 'inst' (before 'ance')
      expectedEnd: 21, // should extend to end of 'instance_name'
    },
    {
      title: 'cursor in middle of label name in label matcher - should extend to end',
      expr: 'metric{instance_name="value"}',
      pos: 11, // cursor after 'inst' (before 'ance')
      expectedEnd: 20, // should extend to end of 'instance_name'
    },
    {
      title: 'cursor in middle of label name in on() modifier - should extend to end',
      expr: 'a / on(instance_name) b',
      pos: 11, // cursor after 'inst' (before 'ance')
      expectedEnd: 20, // should extend to end of 'instance_name'
    },
    {
      title: 'cursor in middle of label name in ignoring() modifier - should extend to end',
      expr: 'a / ignoring(instance_name) b',
      pos: 17, // cursor after 'inst' (before 'ance')
      expectedEnd: 26, // should extend to end of 'instance_name'
    },
    {
      title: 'cursor in middle of function name rate - should extend to end',
      expr: 'rate(foo[5m])',
      pos: 2, // cursor after 'ra' (before 'te')
      expectedEnd: 4, // should extend to end of 'rate'
    },
    {
      title: 'cursor in middle of function name histogram_quantile - should extend to end',
      expr: 'histogram_quantile(0.9, rate(foo[5m]))',
      pos: 10, // cursor after 'histogram_' (before 'quantile')
      expectedEnd: 18, // should extend to end of 'histogram_quantile'
    },
    {
      title: 'cursor in middle of aggregator sum - should extend to end',
      expr: 'sum(rate(foo[5m]))',
      pos: 2, // cursor after 'su' (before 'm')
      expectedEnd: 3, // should extend to end of 'sum'
    },
    {
      title: 'cursor in middle of aggregator count_values - should extend to end',
      expr: 'count_values("label", foo)',
      pos: 6, // cursor after 'count_' (before 'values')
      expectedEnd: 12, // should extend to end of 'count_values'
    },
    {
      title: 'cursor in middle of nested function - should extend to end',
      expr: 'sum(rate(foo[5m]))',
      pos: 6, // cursor after 'ra' inside rate (before 'te')
      expectedEnd: 8, // should extend to end of 'rate'
    },
    {
      title: 'cursor at beginning of aggregator - should extend to end',
      expr: 'avg by (instance) (rate(foo[5m]))',
      pos: 1, // cursor after 'a' (before 'vg')
      expectedEnd: 3, // should extend to end of 'avg'
    },
    {
      title: 'cursor in middle of function name with binary op - should extend to end',
      expr: 'rate(foo[5m]) / irate(bar[5m])',
      pos: 17, // cursor after 'ir' inside irate (before 'ate')
      expectedEnd: 21, // should extend to end of 'irate'
    },
    {
      title: 'error node - returns pos (cursor position)',
      expr: 'metric_name !',
      pos: 13, // cursor at '!' (error node)
      expectedEnd: 13, // error node returns pos
    },
    {
      title: 'end should be equal to the pos after closing function',
      expr: 'rate(foo[5m])',
      pos: 13, // cursor is after the closing bracket
      expectedEnd: 13,
    },
    {
      title: 'end should be equal to the pos after closing aggregation',
      expr: 'sum(rate(foo[5m]))',
      pos: 18, // cursor is after the closing bracket
      expectedEnd: 18,
    },
  ];
  testCases.forEach((value) => {
    it(value.title, () => {
      const state = createEditorState(value.expr);
      const node = syntaxTree(state).resolve(value.pos, -1);
      const result = computeEndCompletePosition(state, node, value.pos);
      expect(result).toEqual(value.expectedEnd);
    });
  });
});

describe('autocomplete promQL test', () => {
  beforeEach(() => {
    mockPrometheusServer();
  });
  const testCases = [
    {
      title: 'offline empty expr',
      expr: '',
      pos: 0,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
        from: 0,
        to: 0,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline simple function/aggregation/number autocompletion',
      expr: 'go_',
      pos: 3,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
        from: 0,
        to: 3,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline function/aggregation autocompletion in aggregation',
      expr: 'sum()',
      pos: 4,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 4,
        to: 4,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete aggregate operation modifier or binary operator after closing aggregation',
      expr: 'sum()',
      pos: 5, // cursor is after the closing bracket
      expectedResult: {
        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
        from: 5,
        to: 5,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete binary operator after closing function',
      expr: 'rate(foo[5m])',
      pos: 13, // cursor is after the closing bracket
      expectedResult: {
        options: binOpTerms,
        from: 13,
        to: 13,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline function/aggregation autocompletion in aggregation 2',
      expr: 'sum(ra)',
      pos: 6,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 4,
        to: 6,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'cursor in middle of metric name - to should extend to end (issue #15839)',
      expr: 'sum(coredns_cache_hits_total)',
      pos: 18, // cursor is after 'coredns_cache_' (before 'hits')
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 4,
        to: 28, // should extend to end of 'coredns_cache_hits_total'
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'cursor in middle of metric name inside rate() - to should extend to end (issue #15839)',
      expr: 'rate(coredns_cache_hits_total[2m])',
      pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 5,
        to: 29, // should extend to end of 'coredns_cache_hits_total'
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline function/aggregation autocompletion in aggregation 3',
      expr: 'sum(rate())',
      pos: 9,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 9,
        to: 9,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete aggregate operation modifier or binary operator after closing nested aggregation',
      expr: 'sum(rate(foo[5m]))',
      pos: 18, // cursor is after the closing bracket
      expectedResult: {
        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
        from: 18,
        to: 18,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline function/aggregation autocompletion in aggregation 4',
      expr: 'sum by (instance, job) ( sum_over(scrape_series_added[1h])) / sum by (instance, job) (sum_over_time(scrape_samples_scraped[1h])) > 0.1 and sum by(instance, job) (scrape_samples_scraped{) > 100',
      pos: 33,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
        from: 25,
        to: 33,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'autocomplete binOp modifier/metric/number',
      expr: 'metric_name / ignor',
      pos: 19,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
        from: 14,
        to: 19,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'autocomplete binOp modifier/metric/number 2',
      expr: 'sum(http_requests_total{method="GET"} / o)',
      pos: 41,
      expectedResult: {
        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
        from: 40,
        to: 41,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing',
      expr: 'sum by ()',
      pos: 8, // cursor is between the bracket
      expectedResult: {
        options: [],
        from: 8,
        to: 8,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 2',
      expr: 'sum by (myL)',
      pos: 11, // cursor is between the bracket after the string myL
      expectedResult: {
        options: [],
        from: 8,
        to: 11,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 3',
      expr: 'sum by (myLabel1, myLab)',
      pos: 23, // cursor is between the bracket after the string myLab
      expectedResult: {
        options: [],
        from: 18,
        to: 23,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 4',
      expr: 'metric_name{}',
      pos: 12, // cursor is between the bracket
      expectedResult: {
        options: [],
        from: 12,
        to: 12,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 5',
      expr: '{}',
      pos: 1, // cursor is between the bracket
      expectedResult: {
        options: [],
        from: 1,
        to: 1,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 6',
      expr: 'metric_name{myL}',
      pos: 15, // cursor is between the bracket after the string myL
      expectedResult: {
        options: [],
        from: 12,
        to: 15,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelName return nothing 7',
      expr: '{myL}',
      pos: 4, // cursor is between the bracket after the string myL
      expectedResult: {
        options: [],
        from: 1,
        to: 4,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelValue return nothing',
      expr: 'metric_name{labelName=""}',
      pos: 23, // cursor is between the quotes
      expectedResult: {
        options: [],
        from: 23,
        to: 23,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete labelValue return nothing 2',
      expr: '{labelName=""}',
      pos: 12, // cursor is between the quotes
      expectedResult: {
        options: [],
        from: 12,
        to: 12,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete aggregate operation modifier or binary operator',
      expr: 'sum() b',
      pos: 7, // cursor is after 'b'
      expectedResult: {
        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
        from: 6,
        to: 7,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete aggregate operation modifier or binary operator 2',
      expr: 'sum(rate(foo[5m])) an',
      pos: 21, // cursor is after the string 'an'
      expectedResult: {
        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
        from: 19,
        to: 21,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete aggregate operation modifier or binary operator or offset',
      expr: 'sum b',
      pos: 5, // cursor is after 'b'
      expectedResult: {
        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms, [{ label: 'offset' }]),
        from: 4,
        to: 5,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete binOp',
      expr: 'metric_name !',
      pos: 13,
      expectedResult: {
        options: binOpTerms,
        from: 12,
        to: 13,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete binOp 2',
      expr: 'metric_name =',
      pos: 13,
      expectedResult: {
        options: binOpTerms,
        from: 12,
        to: 13,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete matchOp',
      expr: 'go{instance=""}',
      pos: 12, // cursor is after the 'equal'
      expectedResult: {
        options: matchOpTerms,
        from: 11,
        to: 12,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete matchOp 2',
      expr: 'metric_name{labelName!}',
      pos: 22, // cursor is after '!'
      expectedResult: {
        options: matchOpTerms,
        from: 21,
        to: 22,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete duration with offset',
      expr: 'http_requests_total offset 5',
      pos: 28,
      expectedResult: {
        options: durationTerms,
        from: 28,
        to: 28,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration with offset 2',
      expr: 'sum(http_requests_total{method="GET"} offset 4)',
      pos: 46,
      expectedResult: {
        options: durationTerms,
        from: 46,
        to: 46,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete offset or binOp',
      expr: 'http_requests_total off',
      pos: 23,
      expectedResult: {
        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
        from: 20,
        to: 23,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete offset or binOp 2',
      expr: 'metric_name unle',
      pos: 16,
      expectedResult: {
        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
        from: 12,
        to: 16,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete offset or binOp 3',
      expr: 'http_requests_total{method="GET"} off',
      pos: 37,
      expectedResult: {
        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
        from: 34,
        to: 37,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete offset or binOp 4',
      expr: 'rate(foo[5m]) un',
      pos: 16,
      expectedResult: {
        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
        from: 14,
        to: 16,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'autocomplete offset or binop 5',
      expr: 'sum(http_requests_total{method="GET"} off)',
      pos: 41,
      expectedResult: {
        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
        from: 38,
        to: 41,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline not autocompleting duration for a matrixSelector',
      expr: 'go[]',
      pos: 3,
      expectedResult: {
        options: [],
        from: 0,
        to: 3,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline not autocompleting duration for a matrixSelector 2',
      expr: 'go{l1="l2"}[]',
      pos: 12,
      expectedResult: {
        options: [],
        from: 0,
        to: 12,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete duration for a matrixSelector',
      expr: 'go[5]',
      pos: 4,
      expectedResult: {
        options: durationTerms,
        from: 4,
        to: 4,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a matrixSelector 2',
      expr: 'go[5d1]',
      pos: 6,
      expectedResult: {
        options: durationTerms,
        from: 6,
        to: 6,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a matrixSelector 3',
      expr: 'rate(my_metric{l1="l2"}[25])',
      pos: 26,
      expectedResult: {
        options: durationTerms,
        from: 26,
        to: 26,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a matrixSelector 4',
      expr: 'rate(my_metric{l1="l2"}[25d5])',
      pos: 28,
      expectedResult: {
        options: durationTerms,
        from: 28,
        to: 28,
        validFor: undefined,
      },
    },
    {
      title: 'offline do not autocomplete duration when unit already present in matrixSelector',
      expr: 'rate(foo[5m])',
      pos: 10,
      expectedResult: {
        options: [],
        from: 10,
        to: 11,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline do not autocomplete duration when multi char unit already present in matrixSelector',
      expr: 'rate(foo[5ms])',
      pos: 10,
      expectedResult: {
        options: [],
        from: 10,
        to: 12,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'offline autocomplete duration for a subQuery',
      expr: 'go[5d:5]',
      pos: 7,
      expectedResult: {
        options: durationTerms,
        from: 7,
        to: 7,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a subQuery 2',
      expr: 'go[5d:5d4]',
      pos: 9,
      expectedResult: {
        options: durationTerms,
        from: 9,
        to: 9,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a subQuery 3',
      expr: 'rate(my_metric{l1="l2"}[25d:6])',
      pos: 29,
      expectedResult: {
        options: durationTerms,
        from: 29,
        to: 29,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete duration for a subQuery 4',
      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
      pos: 31,
      expectedResult: {
        options: durationTerms,
        from: 31,
        to: 31,
        validFor: undefined,
      },
    },
    {
      title: 'offline autocomplete at modifiers',
      expr: '1 @ s',
      pos: 5,
      expectedResult: {
        options: atModifierTerms,
        from: 4,
        to: 5,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'online autocomplete of metrics',
      expr: 'alert',
      pos: 5,
      conf: { remote: { url: 'http://localhost:8080' } },
      expectedResult: {
        options: ([] as Completion[]).concat(mockedMetricsTerms, functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
        from: 0,
        to: 5,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'online autocomplete of label name corresponding to a metric',
      expr: 'alertmanager_alerts{}',
      pos: 20,
      conf: { remote: { url: 'http://localhost:8080' } },
      expectedResult: {
        options: [
          {
            label: 'env',
            type: 'constant',
          },
          {
            label: 'instance',
            type: 'constant',
          },
          {
            label: 'job',
            type: 'constant',
          },
          {
            label: 'state',
            type: 'constant',
          },
        ],
        from: 20,
        to: 20,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'online autocomplete of label value corresponding to a metric and a label name',
      expr: 'alertmanager_alerts{env=""}',
      pos: 25,
      conf: { remote: { url: 'http://localhost:8080' } },
      expectedResult: {
        options: [
          {
            label: 'demo',
            apply: 'demo',
            type: 'text',
          },
          {
            label: '\\x2d',
            apply: '\\\\x2d',
            type: 'text',
          },
          {
            label: 'quoted"value',
            apply: 'quoted\\"value',
            type: 'text',
          },
        ],
        from: 25,
        to: 25,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
    {
      title: 'online autocomplete with initial metric list',
      expr: 'rat',
      pos: 3,
      conf: { remote: { cache: { initialMetricList: ['metric1', 'metric2', 'rat'] } } },
      expectedResult: {
        options: ([] as Completion[]).concat(
          [
            {
              label: 'metric1',
              type: 'constant',
            },
            {
              label: 'metric2',
              type: 'constant',
            },
            {
              label: 'rat',
              type: 'constant',
            },
          ],
          functionIdentifierTerms,
          aggregateOpTerms,
          numberTerms,
          snippets
        ),
        from: 0,
        to: 3,
        validFor: /^[a-zA-Z0-9_:]+$/,
      },
    },
  ];
  testCases.forEach((value) => {
    it(value.title, async () => {
      const state = createEditorState(value.expr);
      const context = new CompletionContext(state, value.pos, true);
      const completion = newCompleteStrategy(value.conf);
      const result = await completion.promQL(context);
      expect(result).toEqual(value.expectedResult);
    });
  });

  it('online autocomplete of openmetrics counter', async () => {
    const metricName = 'direct_notifications_total';
    const baseMetricName = 'direct_notifications';
    nock('http://localhost:8080')
      .get('/api/v1/label/__name__/values')
      .query(true)
      .reply(200, { status: 'success', data: [metricName] });
    nock('http://localhost:8080')
      .get('/api/v1/metadata')
      .query(true)
      .reply(200, {
        status: 'success',
        data: {
          [baseMetricName]: [
            {
              type: 'counter',
              help: 'Number of direct notifications.',
              unit: '',
            },
          ],
        },
      });
    const state = createEditorState(metricName);
    const context = new CompletionContext(state, metricName.length, true);
    const completion = newCompleteStrategy({ remote: { url: 'http://localhost:8080' } });
    const result = await completion.promQL(context);
    // nock only mocks the HTTP endpoints; this test just ensures remote completion works
    // when metadata for an OpenMetrics _total counter is stored under its base metric name.
    expect(result).not.toBeNull();
    expect((result as NonNullable<typeof result>).options.length).toBeGreaterThan(0);
  });
});
