import { SyntaxNode } from '@lezer/common';

import {
  addLabelFormatToQuery,
  addLabelToQuery,
  addNoPipelineErrorToQuery,
  addParserToQuery,
  NodePosition,
  queryHasFilter,
  removeCommentsFromQuery,
  removeLabelFromQuery,
} from './modifyQuery';

describe('addLabelToQuery()', () => {
  it.each`
    query                                                                                                                             | description                                                                    | label    | operator | value          | expectedResult
    ${'{x="y"}'}                                                                                                                      | ${'no label and value'}                                                        | ${''}    | ${'='}   | ${''}          | ${''}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query'}                                                              | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="yy", bar="baz"}'}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query'}                                                              | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="yy", bar="baz"}'}
    ${'{x="yy"}'}                                                                                                                     | ${'custom operator'}                                                           | ${'bar'} | ${'!='}  | ${'baz'}       | ${'{x="yy", bar!="baz"}'}
    ${'rate({}[1m])'}                                                                                                                 | ${'do not modify ranges'}                                                      | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({bar="baz"}[1m])'}
    ${'sum by (host) (rate({} [1m]))'}                                                                                                | ${'detect in-order function use'}                                              | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum by (host) (rate({bar="baz"}[1m]))'}
    ${'{instance="my-host.com:9100"}'}                                                                                                | ${'selectors with punctuation'}                                                | ${'bar'} | ${'='}   | ${'baz'}       | ${'{instance="my-host.com:9100", bar="baz"}'}
    ${'{list="a,b,c"}'}                                                                                                               | ${'selectors with punctuation'}                                                | ${'bar'} | ${'='}   | ${'baz'}       | ${'{list="a,b,c", bar="baz"}'}
    ${'rate({}[5m]) + rate({}[5m])'}                                                                                                  | ${'arithmetical expressions'}                                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({bar="baz"}[5m]) + rate({bar="baz"}[5m])'}
    ${'avg(rate({x="y"} [$__interval]))+ sum(rate({}[5m]))'}                                                                          | ${'arithmetical expressions'}                                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'avg(rate({x="y", bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'}
    ${'rate({x="yy"}[5m]) * rate({y="zz",a="bb"}[5m]) * rate({}[5m])'}                                                                | ${'arithmetical expressions'}                                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({x="yy", bar="baz"}[5m]) * rate({y="zz", a="bb", bar="baz"}[5m]) * rate({bar="baz"}[5m])'}
    ${'{x="yy", bar!="baz"}'}                                                                                                         | ${'do not add duplicate labels'}                                               | ${'bar'} | ${'!='}  | ${'baz'}       | ${'{x="yy", bar!="baz"}'}
    ${'rate({bar="baz"}[1m])'}                                                                                                        | ${'do not add duplicate labels'}                                               | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({bar="baz"}[1m])'}
    ${'{list="a,b,c", bar="baz"}'}                                                                                                    | ${'do not add duplicate labels'}                                               | ${'bar'} | ${'='}   | ${'baz'}       | ${'{list="a,b,c", bar="baz"}'}
    ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'}                                                             | ${'do not add duplicate labels'}                                               | ${'bar'} | ${'='}   | ${'baz'}       | ${'avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'}
    ${'{x="y"} |="yy"'}                                                                                                               | ${'do not remove filters'}                                                     | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="y", bar="baz"} |="yy"'}
    ${'{x="y"} |="yy" !~"xx"'}                                                                                                        | ${'do not remove filters'}                                                     | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="y", bar="baz"} |="yy" !~"xx"'}
    ${'{x="y"} or {}'}                                                                                                                | ${'metric with logical operators'}                                             | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="y", bar="baz"} or {bar="baz"}'}
    ${'{x="y"} and {}'}                                                                                                               | ${'metric with logical operators'}                                             | ${'bar'} | ${'='}   | ${'baz'}       | ${'{x="y", bar="baz"} and {bar="baz"}'}
    ${'sum(rate({job="foo"}[2m])) by (value $variable)'}                                                                              | ${'template variables'}                                                        | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)'}
    ${'rate({x="y"}[${__range_s}s])'}                                                                                                 | ${'metric query with range grafana variable'}                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({x="y", bar="baz"}[${__range_s}s])'}
    ${'max by (id, name, type) ({type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (rate({} [5m])) * 1000'} | ${'metric query with labels in label list with the group modifier'}            | ${'bar'} | ${'='}   | ${'baz'}       | ${'max by (id, name, type) ({type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (rate({bar="baz"}[5m])) * 1000'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser'}                                                         | ${'bar'} | ${'='}   | ${'baz'}       | ${'{foo="bar"} | logfmt | bar=`baz`'}
    ${'{foo="bar"} | logfmt | json'}                                                                                                  | ${'query with multiple parsers'}                                               | ${'bar'} | ${'='}   | ${'baz'}       | ${'{foo="bar"} | logfmt | json | bar=`baz`'}
    ${'{foo="bar"} | logfmt | x="y"'}                                                                                                 | ${'query with parser and label filter'}                                        | ${'bar'} | ${'='}   | ${'baz'}       | ${'{foo="bar"} | logfmt | x="y" | bar=`baz`'}
    ${'rate({foo="bar"} | logfmt [5m])'}                                                                                              | ${'metric query with parser'}                                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'rate({foo="bar"} | logfmt | bar=`baz` [5m])'}
    ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | line_format "{{.status}}" [5m]))'}                                           | ${'metric query with parser'}                                                  | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum by(host) (rate({foo="bar"} | logfmt | x="y" | bar=`baz` | line_format "{{.status}}" [5m]))'}
    ${'{foo="bar"} | logfmt | line_format "{{.status}}"'}                                                                             | ${'do not add filter to line_format expressions in query with parser'}         | ${'bar'} | ${'='}   | ${'baz'}       | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{.status}}"'}
    ${'{foo="bar"} | logfmt | line_format "{{status}}"'}                                                                              | ${'do not add filter to line_format expressions in query with parser'}         | ${'bar'} | ${'='}   | ${'baz'}       | ${'{foo="bar"} | logfmt | bar=`baz` | line_format "{{status}}"'}
    ${'{}'}                                                                                                                           | ${'query without stream selector'}                                             | ${'bar'} | ${'='}   | ${'baz'}       | ${'{bar="baz"}'}
    ${'{} | logfmt'}                                                                                                                  | ${'query without stream selector and with parser'}                             | ${'bar'} | ${'='}   | ${'baz'}       | ${'{bar="baz"}| logfmt'}
    ${'{} | x="y"'}                                                                                                                   | ${'query without stream selector and with label filter'}                       | ${'bar'} | ${'='}   | ${'baz'}       | ${'{bar="baz"}| x="y"'}
    ${'{} | logfmt | x="y"'}                                                                                                          | ${'query without stream selector and with parser and label filter'}            | ${'bar'} | ${'='}   | ${'baz'}       | ${'{bar="baz"}| logfmt | x="y"'}
    ${'sum(rate({x="y"} [5m])) + sum(rate({} | logfmt [5m]))'}                                                                        | ${'metric query with 1 empty and 1 not empty stream selector with parser'}     | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum(rate({x="y", bar="baz"} [5m])) + sum(rate({bar="baz"}| logfmt [5m]))'}
    ${'sum(rate({x="y"} | logfmt [5m])) + sum(rate({} [5m]))'}                                                                        | ${'metric query with 1 non-empty and 1 not empty stream selector with parser'} | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum(rate({x="y", bar="baz"} | logfmt [5m])) + sum(rate({bar="baz"}[5m]))'}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query with escaped value'}                                           | ${'bar'} | ${'='}   | ${'"baz"'}     | ${'{x="yy", bar=""baz""}'}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query with escaped value'}                                           | ${'bar'} | ${'='}   | ${'\\"baz\\"'} | ${'{x="yy", bar="\\"baz\\""}'}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query with an other escaped value'}                                  | ${'bar'} | ${'='}   | ${'baz\\\\'}   | ${'{x="yy", bar="baz\\\\"}'}
    ${'{x="yy"}'}                                                                                                                     | ${'simple query with escaped value and regex operator'}                        | ${'bar'} | ${'~='}  | ${'baz\\\\'}   | ${'{x="yy", bar~="baz\\\\"}'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser with escaped value'}                                      | ${'bar'} | ${'='}   | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar=`"baz"`'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser with an other escaped value'}                             | ${'bar'} | ${'='}   | ${'baz\\\\'}   | ${'{foo="bar"} | logfmt | bar=`baz\\`'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser with escaped value and regex operator'}                   | ${'bar'} | ${'~='}  | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar~=`"baz"`'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser with escaped value and regex operator'}                   | ${'bar'} | ${'~='}  | ${'\\"baz\\"'} | ${'{foo="bar"} | logfmt | bar~=`"baz"`'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser, > operator and number value'}                            | ${'bar'} | ${'>'}   | ${'5'}         | ${'{foo="bar"} | logfmt | bar>5'}
    ${'{foo="bar"} | logfmt'}                                                                                                         | ${'query with parser, < operator and non-number value'}                        | ${'bar'} | ${'<'}   | ${'5KiB'}      | ${'{foo="bar"} | logfmt | bar<`5KiB`'}
    ${'sum(rate({x="y"} | logfmt [5m])) + sum(rate({x="z"} | logfmt [5m]))'}                                                          | ${'metric query with non empty selectors and parsers'}                         | ${'bar'} | ${'='}   | ${'baz'}       | ${'sum(rate({x="y"} | logfmt | bar=`baz` [5m])) + sum(rate({x="z"} | logfmt | bar=`baz` [5m]))'}
  `(
    'should add label to query:  $query, description: $description',
    ({ query, description, label, operator, value, expectedResult }) => {
      if (description === 'no label and value') {
        expect(() => {
          addLabelToQuery(query, label, operator, value);
        }).toThrow();
      } else {
        expect(addLabelToQuery(query, label, operator, value)).toEqual(expectedResult);
      }
    }
  );

  it('should always add label as labelFilter if force flag is given', () => {
    expect(addLabelToQuery('{foo="bar"}', 'forcedLabel', '=', 'value', true)).toEqual(
      '{foo="bar"} | forcedLabel=`value`'
    );
  });

  it('should always add label as labelFilter if force flag is given with a parser', () => {
    expect(addLabelToQuery('{foo="bar"} | logfmt', 'forcedLabel', '=', 'value', true)).toEqual(
      '{foo="bar"} | logfmt | forcedLabel=`value`'
    );
  });
});

describe('addParserToQuery', () => {
  describe('when query had line filter', () => {
    it('should add parser after line filter', () => {
      expect(addParserToQuery('{job="grafana"} |= "error"', 'logfmt')).toBe('{job="grafana"} |= "error" | logfmt');
    });

    it('should add parser after multiple line filters', () => {
      expect(addParserToQuery('{job="grafana"} |= "error" |= "info" |= "debug"', 'logfmt')).toBe(
        '{job="grafana"} |= "error" |= "info" |= "debug" | logfmt'
      );
    });
  });

  describe('when query has no line filters', () => {
    it('should add parser after log stream selector in logs query', () => {
      expect(addParserToQuery('{job="grafana"}', 'logfmt')).toBe('{job="grafana"} | logfmt');
    });

    it('should add parser after log stream selector in metric query', () => {
      expect(addParserToQuery('rate({job="grafana"} [5m])', 'logfmt')).toBe('rate({job="grafana"} | logfmt [5m])');
    });
  });
});

describe('addNoPipelineErrorToQuery', () => {
  it('should add error filtering after logfmt parser', () => {
    expect(addNoPipelineErrorToQuery('{job="grafana"} | logfmt')).toBe('{job="grafana"} | logfmt | __error__=``');
  });

  it('should add error filtering after json parser with expressions', () => {
    expect(addNoPipelineErrorToQuery('{job="grafana"} | json foo="bar", bar="baz"')).toBe(
      '{job="grafana"} | json foo="bar", bar="baz" | __error__=``'
    );
  });

  it('should not add error filtering if no parser', () => {
    expect(addNoPipelineErrorToQuery('{job="grafana"} |="no parser"')).toBe('{job="grafana"} |="no parser"');
  });
});

describe('addLabelFormatToQuery', () => {
  it('should add label format at the end of log query when parser', () => {
    expect(addLabelFormatToQuery('{job="grafana"} | logfmt', { originalLabel: 'lvl', renameTo: 'level' })).toBe(
      '{job="grafana"} | logfmt | label_format level=lvl'
    );
  });

  it('should add label format at the end of log query when no parser', () => {
    expect(addLabelFormatToQuery('{job="grafana"}', { originalLabel: 'lvl', renameTo: 'level' })).toBe(
      '{job="grafana"} | label_format level=lvl'
    );
  });

  it('should add label format at the end of log query when more label parser', () => {
    expect(
      addLabelFormatToQuery('{job="grafana"} | logfmt | label_format a=b', { originalLabel: 'lvl', renameTo: 'level' })
    ).toBe('{job="grafana"} | logfmt | label_format a=b | label_format level=lvl');
  });

  it('should add label format at the end of log query part of metrics query', () => {
    expect(
      addLabelFormatToQuery('rate({job="grafana"} | logfmt | label_format a=b [5m])', {
        originalLabel: 'lvl',
        renameTo: 'level',
      })
    ).toBe('rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m])');
  });

  it('should add label format at the end of multiple log query part of metrics query', () => {
    expect(
      addLabelFormatToQuery(
        'rate({job="grafana"} | logfmt | label_format a=b [5m]) + rate({job="grafana"} | logfmt | label_format a=b [5m])',
        { originalLabel: 'lvl', renameTo: 'level' }
      )
    ).toBe(
      'rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m]) + rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m])'
    );
  });
});

describe('removeCommentsFromQuery', () => {
  it.each`
    query                                                                             | expectedResult
    ${'{job="grafana"}#hello'}                                                        | ${'{job="grafana"}'}
    ${'{job="grafana"} | logfmt #hello'}                                              | ${'{job="grafana"} | logfmt '}
    ${'{job="grafana", bar="baz"} |="test" | logfmt | label_format level=lvl #hello'} | ${'{job="grafana", bar="baz"} |="test" | logfmt | label_format level=lvl '}
    ${`#sum(rate(\n{host="containers"}\n#[1m]))`}                                     | ${`\n{host="containers"}\n`}
    ${`#sum(rate(\n{host="containers"}\n#| logfmt\n#[1m]))`}                          | ${`\n{host="containers"}\n\n`}
    ${'{job="grafana"}\n#hello\n| logfmt'}                                            | ${'{job="grafana"}\n\n| logfmt'}
  `('strips comments in log query:  {$query}', ({ query, expectedResult }) => {
    expect(removeCommentsFromQuery(query)).toBe(expectedResult);
  });

  it.each`
    query                                                                      | expectedResult
    ${'{job="grafana"}'}                                                       | ${'{job="grafana"}'}
    ${'{job="grafana"} | logfmt'}                                              | ${'{job="grafana"} | logfmt'}
    ${'{job="grafana", bar="baz"} |="test" | logfmt | label_format level=lvl'} | ${'{job="grafana", bar="baz"} |="test" | logfmt | label_format level=lvl'}
  `('returns original query if no comments in log query:  {$query}', ({ query, expectedResult }) => {
    expect(removeCommentsFromQuery(query)).toBe(expectedResult);
  });

  it.each`
    query                                                       | expectedResult
    ${'count_over_time({job="grafana"}[10m])#hello'}            | ${'count_over_time({job="grafana"}[10m])'}
    ${'count_over_time({job="grafana"} | logfmt[10m])#hello'}   | ${'count_over_time({job="grafana"} | logfmt[10m])'}
    ${'rate({job="grafana"} | logfmt | foo="bar" [10m])#hello'} | ${'rate({job="grafana"} | logfmt | foo="bar" [10m])'}
  `('strips comments in metrics query:  {$query}', ({ query, expectedResult }) => {
    expect(removeCommentsFromQuery(query)).toBe(expectedResult);
  });

  it.each`
    query                                                     | expectedResult
    ${'count_over_time({job="grafana"}[10m])#hello'}          | ${'count_over_time({job="grafana"}[10m])'}
    ${'count_over_time({job="grafana"} | logfmt[10m])#hello'} | ${'count_over_time({job="grafana"} | logfmt[10m])'}
    ${'rate({job="grafana"} | logfmt | foo="bar" [10m])'}     | ${'rate({job="grafana"} | logfmt | foo="bar" [10m])'}
  `('returns original query if no comments in metrics query:  {$query}', ({ query, expectedResult }) => {
    expect(removeCommentsFromQuery(query)).toBe(expectedResult);
  });
});

describe('NodePosition', () => {
  describe('contains', () => {
    it('should return true if the position is contained within the current position', () => {
      const position = new NodePosition(5, 10);
      const containedPosition = new NodePosition(6, 9);
      const result = position.contains(containedPosition);
      expect(result).toBe(true);
    });

    it('should return false if the position is not contained within the current position', () => {
      const position = new NodePosition(5, 10);
      const outsidePosition = new NodePosition(11, 15);
      const result = position.contains(outsidePosition);
      expect(result).toBe(false);
    });

    it('should return true if the position is the same as the current position', () => {
      const position = new NodePosition(5, 10);
      const samePosition = new NodePosition(5, 10);
      const result = position.contains(samePosition);
      expect(result).toBe(true);
    });
  });

  describe('getExpression', () => {
    it('should return the substring of the query within the given position', () => {
      const position = new NodePosition(7, 12);
      const query = 'Hello, world!';
      const result = position.getExpression(query);
      expect(result).toBe('world');
    });

    it('should return an empty string if the position is out of range', () => {
      const position = new NodePosition(15, 20);
      const query = 'Hello, world!';
      const result = position.getExpression(query);
      expect(result).toBe('');
    });
  });

  describe('fromNode', () => {
    it('should create a new NodePosition instance from a SyntaxNode', () => {
      const syntaxNode = {
        from: 5,
        to: 10,
        type: 'identifier',
      } as unknown as SyntaxNode;
      const result = NodePosition.fromNode(syntaxNode);
      expect(result).toBeInstanceOf(NodePosition);
      expect(result.from).toBe(5);
      expect(result.to).toBe(10);
      expect(result.type).toBe('identifier');
    });
  });
});

describe('queryHasFilter', () => {
  it.each([
    ['{job="grafana"}', 'grafana'],
    ['{job="grafana", foo="bar"}', 'grafana'],
    ['{foo="bar", job="grafana"}', 'grafana'],
    ['{job="\\"grafana\\""}', '"grafana"'],
    ['{foo="bar"} | logfmt | job=`grafana`', 'grafana'],
  ])('should return true if query has a positive filter', (query: string, value: string) => {
    expect(queryHasFilter(query, 'job', '=', value)).toBe(true);
  });

  it.each([
    ['{job!="grafana"}', 'grafana'],
    ['{job!="grafana", foo="bar"}', 'grafana'],
    ['{foo="bar", job!="grafana"}', 'grafana'],
    ['{job!="\\"grafana\\""}', '"grafana"'],
    ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana'],
  ])('should return true if query has a negative filter', (query: string, value: string) => {
    expect(queryHasFilter(query, 'job', '!=', value)).toBe(true);
  });
});

describe('removeLabelFromQuery', () => {
  it.each([
    ['{job="grafana"}', 'grafana', '{}'],
    ['{job="grafana", foo="bar"}', 'grafana', '{foo="bar"}'],
    ['{foo="bar", job="grafana"}', 'grafana', '{foo="bar"}'],
    ['{job="\\"grafana\\""}', '"grafana"', '{}'],
    ['{foo="bar"} | logfmt | job=`grafana`', 'grafana', '{foo="bar"} | logfmt'],
  ])('should remove a positive label matcher from the query', (query: string, value: string, expected: string) => {
    expect(removeLabelFromQuery(query, 'job', '=', value)).toBe(expected);
  });

  it.each([
    ['{job!="grafana"}', 'grafana', '{}'],
    ['{job!="grafana", foo="bar"}', 'grafana', '{foo="bar"}'],
    ['{foo="bar", job!="grafana"}', 'grafana', '{foo="bar"}'],
    ['{job!="\\"grafana\\""}', '"grafana"', '{}'],
    ['{foo="bar"} | logfmt | job!=`grafana`', 'grafana', '{foo="bar"} | logfmt'],
  ])('should remove a negative label matcher from the query', (query: string, value: string, expected: string) => {
    expect(removeLabelFromQuery(query, 'job', '!=', value)).toBe(expected);
  });
});
