import { CustomVariableModel } from '@grafana/data';
import { Monaco, monacoTypes } from '@grafana/ui';

import { LogGroup } from '../../../dataquery.gen';
import { setupMockedTemplateService, logGroupNamesVariable } from '../../../mocks/CloudWatchDataSource';
import { multiLineFullQuery } from '../../../mocks/cloudwatch-logs-sql-test-data/multiLineFullQuery';
import { multiLineFullQueryWithCaseClause } from '../../../mocks/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause';
import { partialQueryWithSubquery } from '../../../mocks/cloudwatch-logs-sql-test-data/partialQueryWithSubquery';
import { singleLineFullQuery } from '../../../mocks/cloudwatch-logs-sql-test-data/singleLineFullQuery';
import { whitespaceQuery } from '../../../mocks/cloudwatch-logs-sql-test-data/whitespaceQuery';
import MonacoMock from '../../../mocks/monarch/Monaco';
import TextModel from '../../../mocks/monarch/TextModel';
import { ResourcesAPI } from '../../../resources/ResourcesAPI';
import { ResourceResponse } from '../../../resources/types';
import { LogGroupField } from '../../../types';
import cloudWatchLogsLanguageDefinition from '../definition';
import {
  SELECT,
  ALL,
  DISTINCT,
  ALL_FUNCTIONS,
  FROM,
  BY,
  WHERE,
  GROUP,
  ORDER,
  LIMIT,
  INNER,
  LEFT,
  OUTER,
  JOIN,
  ON,
  HAVING,
  PREDICATE_OPERATORS,
  LOGICAL_OPERATORS,
  ASC,
  DESC,
  CASE,
  WHEN,
  THEN,
  ELSE,
  END,
} from '../language';

import { LogsSQLCompletionItemProvider } from './CompletionItemProvider';

jest.mock('monaco-editor/esm/vs/editor/editor.api', () => ({
  Token: jest.fn((offset, type, language) => ({ offset, type, language })),
}));

const getSuggestions = async (
  value: string,
  position: monacoTypes.IPosition,
  variables: CustomVariableModel[] = [],
  logGroups: LogGroup[] = [],
  fields: Array<ResourceResponse<LogGroupField>> = []
) => {
  const setup = new LogsSQLCompletionItemProvider(
    {
      getActualRegion: () => 'us-east-2',
    } as ResourcesAPI,
    setupMockedTemplateService(variables),
    { region: 'default', logGroups }
  );

  setup.resources.getLogGroupFields = jest.fn().mockResolvedValue(fields);
  const monaco = MonacoMock as Monaco;
  const provider = setup.getCompletionProvider(monaco, cloudWatchLogsLanguageDefinition);
  const { suggestions } = await provider.provideCompletionItems(
    TextModel(value) as monacoTypes.editor.ITextModel,
    position
  );
  return suggestions;
};

describe('LogsSQLCompletionItemProvider', () => {
  describe('getSuggestions', () => {
    it('returns select keyword for an empty query', async () => {
      const suggestions = await getSuggestions(whitespaceQuery.query, { lineNumber: 1, column: 0 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([SELECT]));
    });

    it('returns `ALL`, `DISTINCT`, `CASE`, keywords and all functions after select keyword', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 7 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([ALL, DISTINCT, CASE, ...ALL_FUNCTIONS]));
    });

    it('returns `CASE` keyword and all functions for a select expression', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 37 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([CASE, ...ALL_FUNCTIONS]));
    });

    it('returns `FROM`, `CASE` keywords and all functions after a select expression', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 103 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(
        expect.arrayContaining([
          FROM,
          `${FROM} \`$__logGroups\``,
          `${FROM} \`logGroups(logGroupIdentifier: [...])\``,
          CASE,
          ...ALL_FUNCTIONS,
        ])
      );
    });

    it('returns logGroups and $__logGroups suggestion after from keyword', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 108 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(
        expect.arrayContaining(['`$__logGroups`', '`logGroups(logGroupIdentifier: [...])`'])
      );
    });

    it('returns where, having, limit, group by, order by, and join suggestions after from arguments', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 125 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(
        expect.arrayContaining([
          WHERE,
          HAVING,
          LIMIT,
          `${GROUP} ${BY}`,
          `${ORDER} ${BY}`,
          `${INNER} ${JOIN} <log group> ${ON} <field>`,
          `${LEFT} ${OUTER} ${JOIN} <log group> ${ON} <field>`,
        ])
      );
    });

    it('returns `CASE` keyword and all functions for a where key', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 182 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([CASE, ...ALL_FUNCTIONS]));
    });

    it('returns predicate operators after a where key', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 191 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS));
    });

    it('returns all functions after a where comparison operator', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 193 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns logical operators, group by, order by, and limit keywords after a where value', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 201 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(
        expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${GROUP} ${BY}`, `${ORDER} ${BY}`])
      );
    });

    it('returns all functions for a having key', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 7 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns predicate operators after a having key', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 13 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS));
    });

    it('returns all functions after a having comparison operator', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 15 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns logical operators, order by, and limit keywords after a having value', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 8, column: 18 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${ORDER} ${BY}`]));
    });

    it('returns all functions for an on key', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 156 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns predicate operators after an on key', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 165 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(PREDICATE_OPERATORS));
    });

    it('returns all functions after an on comparison operator', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 167 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns logical operators, group by, order by, and limit keywords after an on value', async () => {
      const suggestions = await getSuggestions(singleLineFullQuery.query, { lineNumber: 1, column: 176 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(
        expect.arrayContaining([...LOGICAL_OPERATORS, LIMIT, `${GROUP} ${BY}`, `${ORDER} ${BY}`])
      );
    });

    it('returns `WHEN` keyword and all functions for a case key', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 5 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, ...ALL_FUNCTIONS]));
    });

    it('returns `WHEN` keyword and predicate operators after a case key', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 7 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, ...PREDICATE_OPERATORS]));
    });

    it('returns all functions after a case comparison operator', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 9 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns `WHEN` keyword after a case value', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 9, column: 11 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN]));
    });

    it('returns all functions for a when key', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 5 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns `THEN` keyword and predicate operators after a when key', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 8 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([THEN, ...PREDICATE_OPERATORS]));
    });

    it('returns all functions after a when comparison operator', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 10 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns `THEN` keyword after a when value', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 14 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([THEN]));
    });

    it('returns all functions after a then keyword', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 19 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns `WHEN`, `ELSE`, and `END` keywords after a then expression', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 4, column: 29 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([WHEN, `${ELSE} ... ${END}`]));
    });

    it('returns all functions after an else keyword', async () => {
      const suggestions = await getSuggestions(multiLineFullQueryWithCaseClause.query, { lineNumber: 5, column: 5 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns all functions after group by keywords', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 7, column: 9 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(ALL_FUNCTIONS));
    });

    it('returns having, limit, order by suggestions after group by', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 7, column: 27 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([HAVING, LIMIT, `${ORDER} ${BY}`]));
    });

    it('returns order directions and limit keywords after order by keywords', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 9, column: 9 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([LIMIT, ASC, DESC]));
    });

    it('returns limit keyword after order by direction', async () => {
      const suggestions = await getSuggestions(multiLineFullQuery.query, { lineNumber: 9, column: 26 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([LIMIT]));
    });

    it('returns `SELECT` keyword and all functions at the start of a subquery', async () => {
      const suggestions = await getSuggestions(partialQueryWithSubquery.query, { lineNumber: 1, column: 53 });
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining([SELECT, ...ALL_FUNCTIONS]));
    });

    it('returns template variables appended to list of suggestions', async () => {
      const suggestions = await getSuggestions(whitespaceQuery.query, { lineNumber: 1, column: 0 }, [
        logGroupNamesVariable,
      ]);
      const suggestionLabels = suggestions.map((s) => s.label);
      const expectedTemplateVariableLabel = `$${logGroupNamesVariable.name}`;
      const expectedLabels = [SELECT, expectedTemplateVariableLabel];
      expect(suggestionLabels).toEqual(expect.arrayContaining(expectedLabels));
    });

    it('fetches fields when logGroups are set', async () => {
      const suggestions = await getSuggestions(
        singleLineFullQuery.query,
        { lineNumber: 1, column: 37 },
        [],
        [{ arn: 'foo', name: 'bar' }],
        [{ value: { name: '@field' } }]
      );
      const suggestionLabels = suggestions.map((s) => s.label);
      expect(suggestionLabels).toEqual(expect.arrayContaining(['@field']));
    });
  });
});
