import { CompletionItem, Position } from 'vscode-languageserver-protocol';
import { CompletionsProvider } from '../completions';
import { expect } from 'vitest';
import { AsyncExpectationResult, MatcherState, RawMatcherFn } from '@vitest/expect';

export type CompleteContext = { relativePath: string; source: string };

interface CustomMatchers<R = unknown> {
  /**
   * @param source {string | CompleteContext} - the source to complete
   * @param completionItem {CompletionItem[] | string[]} - the list of completion items or labels
   *
   * @example
   * expect(provider).to.complete("{% end", ["endcomment"])
   * expect(provider).to.complete({ source: "{% end", relativePath: 'sections/foo.liquid' }, ["endcomment"])
   */
  complete(
    source: string | CompleteContext,
    completionItem: Partial<CompletionItem>[] | string[],
  ): Promise<void>;
}

declare module 'vitest' {
  interface Assertion<T = any> extends CustomMatchers<T> {}
  interface AsymmetricMatchersContaining extends CustomMatchers {}
}

export const complete: RawMatcherFn<MatcherState> = async function (
  this: MatcherState,
  provider: CompletionsProvider,
  context: string | CompleteContext,
  expected: any[] | ReturnType<typeof expect.arrayContaining>,
): AsyncExpectationResult {
  const { isNot, equals, utils } = this;
  const completeContext = asCompleteContextObject(context);
  const source = createSourceCode(completeContext);
  const completionParams = createCompletionParams(completeContext);
  provider.documentManager.open(completionParams.textDocument.uri, source, 1);
  const result = await provider.completions(completionParams);

  let pass = false;
  if ('asymmetricMatch' in expected) {
    pass = equals(result, expected);
  } else if (Array.isArray(expected)) {
    pass =
      result.length === expected.length &&
      expected.every((expectation, i) =>
        equals(
          result[i],
          typeof expectation === 'string'
            ? expect.objectContaining({
                label: expectation,
              })
            : expectation,
        ),
      );
  } else {
    throw new Error('Expecting array or expect.arrayContaining');
  }

  return {
    pass,
    message: () =>
      `expected complete to${isNot ? ' not' : ''} match value ${utils.printExpected(
        expected,
      )}\ncontext:\n${context}`,
    actual: result,
    expected: expected,
  };
};

function asCompleteContextObject(context: string | CompleteContext): CompleteContext {
  if (typeof context === 'string') return { source: context, relativePath: 'file.liquid' };
  return context;
}

function createSourceCode(context: CompleteContext) {
  const regex = new RegExp('█', 'g');
  return context.source.replace(regex, '');
}

function createCompletionParams(context: CompleteContext) {
  const position = createPosition(context.source);
  const textDocument = { uri: `/path/to/${context.relativePath}` };

  return {
    position,
    textDocument,
  };
}

function createPosition(context: string): Position {
  const index = context.indexOf('█');
  const lines = context.substring(0, index === -1 ? context.length : index).split('\n');
  const line = lines.length - 1;
  const character = lines[line].length;

  return { line, character };
}
