// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { SessionContext, ISessionContext } from '@jupyterlab/apputils';

import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';

import { CodeMirrorEditor } from '@jupyterlab/codemirror';

import {
  Completer,
  CompletionHandler,
  CompleterModel,
  KernelConnector
} from '@jupyterlab/completer';

import { createSessionContext } from '@jupyterlab/testutils';

function createEditorWidget(): CodeEditorWrapper {
  const model = new CodeEditor.Model();
  const factory = (options: CodeEditor.IOptions) => {
    return new CodeMirrorEditor(options);
  };
  return new CodeEditorWrapper({ factory, model });
}

class TestCompleterModel extends CompleterModel {
  methods: string[] = [];

  createPatch(patch: string): Completer.IPatch | undefined {
    this.methods.push('createPatch');
    return super.createPatch(patch);
  }

  handleTextChange(change: Completer.ITextState): void {
    this.methods.push('handleTextChange');
    super.handleTextChange(change);
  }
}

class TestCompletionHandler extends CompletionHandler {
  methods: string[] = [];

  onTextChanged(): void {
    super.onTextChanged();
    this.methods.push('onTextChanged');
  }

  onCompletionSelected(widget: Completer, value: string): void {
    super.onCompletionSelected(widget, value);
    this.methods.push('onCompletionSelected');
  }
}

describe('@jupyterlab/completer', () => {
  let connector: KernelConnector;
  let sessionContext: ISessionContext;

  beforeAll(async () => {
    sessionContext = await createSessionContext();
    await (sessionContext as SessionContext).initialize();
    connector = new KernelConnector({ session: sessionContext.session });
  });

  afterAll(() => sessionContext.shutdown());

  describe('CompletionHandler', () => {
    describe('#constructor()', () => {
      it('should create a completer handler', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler).toBeInstanceOf(CompletionHandler);
      });
    });

    describe('#connector', () => {
      it('should be a data connector', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler.connector).toHaveProperty('fetch');
        expect(handler.connector).toHaveProperty('remove');
        expect(handler.connector).toHaveProperty('save');
      });
    });

    describe('#editor', () => {
      it('should default to null', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler.editor).toBeNull();
      });

      it('should be settable', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        const widget = createEditorWidget();
        expect(handler.editor).toBeNull();
        handler.editor = widget.editor;
        expect(handler.editor).toBe(widget.editor);
      });

      it('should be resettable', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        const one = createEditorWidget();
        const two = createEditorWidget();
        expect(handler.editor).toBeNull();
        handler.editor = one.editor;
        expect(handler.editor).toBe(one.editor);
        handler.editor = two.editor;
        expect(handler.editor).toBe(two.editor);
      });

      it('should remove the completer active and enabled classes of the old editor', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        const widget = createEditorWidget();
        handler.editor = widget.editor;
        widget.toggleClass('jp-mod-completer-enabled');
        widget.toggleClass('jp-mod-completer-active');
        handler.editor = null;
        expect(widget.hasClass('jp-mod-completer-enabled')).toBe(false);
        expect(widget.hasClass('jp-mod-completer-active')).toBe(false);
      });
    });

    describe('#isDisposed', () => {
      it('should be true if handler has been disposed', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler.isDisposed).toBe(false);
        handler.dispose();
        expect(handler.isDisposed).toBe(true);
      });
    });

    describe('#dispose()', () => {
      it('should dispose of the handler resources', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler.isDisposed).toBe(false);
        handler.dispose();
        expect(handler.isDisposed).toBe(true);
      });

      it('should be safe to call multiple times', () => {
        const handler = new CompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        expect(handler.isDisposed).toBe(false);
        handler.dispose();
        handler.dispose();
        expect(handler.isDisposed).toBe(true);
      });
    });

    describe('#onTextChanged()', () => {
      it('should fire when the active editor emits a text change', () => {
        const handler = new TestCompletionHandler({
          connector,
          completer: new Completer({ editor: null })
        });
        handler.editor = createEditorWidget().editor;
        expect(handler.methods).toEqual(
          expect.not.arrayContaining(['onTextChanged'])
        );
        handler.editor.model.value.text = 'foo';
        expect(handler.methods).toEqual(
          expect.arrayContaining(['onTextChanged'])
        );
      });

      it('should call model change handler if model exists', () => {
        const completer = new Completer({
          editor: null,
          model: new TestCompleterModel()
        });
        const handler = new TestCompletionHandler({ completer, connector });
        const editor = createEditorWidget().editor;
        const model = completer.model as TestCompleterModel;

        handler.editor = editor;
        expect(model.methods).toEqual(
          expect.not.arrayContaining(['handleTextChange'])
        );
        editor.model.value.text = 'bar';
        editor.setCursorPosition({ line: 0, column: 2 });
        // This signal is emitted (again) because the cursor position that
        // a natural user would create need to be recreated here.
        (editor.model.value.changed as any).emit({ type: 'set', value: 'bar' });
        expect(model.methods).toEqual(
          expect.arrayContaining(['handleTextChange'])
        );
      });
    });

    describe('#onCompletionSelected()', () => {
      it('should fire when the completer widget emits a signal', () => {
        const completer = new Completer({ editor: null });
        const handler = new TestCompletionHandler({ completer, connector });

        expect(handler.methods).toEqual(
          expect.not.arrayContaining(['onCompletionSelected'])
        );
        (completer.selected as any).emit('foo');
        expect(handler.methods).toEqual(
          expect.arrayContaining(['onCompletionSelected'])
        );
      });

      it('should call model create patch method if model exists', () => {
        const completer = new Completer({
          editor: null,
          model: new TestCompleterModel()
        });
        const handler = new TestCompletionHandler({ completer, connector });
        const model = completer.model as TestCompleterModel;

        handler.editor = createEditorWidget().editor;
        expect(model.methods).toEqual(
          expect.not.arrayContaining(['createPatch'])
        );
        (completer.selected as any).emit('foo');
        expect(model.methods).toEqual(expect.arrayContaining(['createPatch']));
      });

      it('should update cell if patch exists', () => {
        const model = new CompleterModel();
        const patch = 'foobar';
        const completer = new Completer({ editor: null, model });
        const handler = new TestCompletionHandler({ completer, connector });
        const editor = createEditorWidget().editor;
        const text = 'eggs\nfoo # comment\nbaz';
        const want = 'eggs\nfoobar # comment\nbaz';
        const line = 1;
        const column = 5;
        const request: Completer.ITextState = {
          column,
          line,
          lineHeight: 0,
          charWidth: 0,
          coords: null,
          text
        };

        handler.editor = editor;
        handler.editor.model.value.text = text;
        handler.editor.setCursorPosition({ line, column: column + 3 });
        model.original = request;
        model.cursor = { start: column, end: column + 3 };
        (completer.selected as any).emit(patch);
        expect(handler.editor.model.value.text).toBe(want);
        expect(handler.editor.getCursorPosition()).toEqual({
          line,
          column: column + 6
        });
      });

      it('should be undoable and redoable', () => {
        const model = new CompleterModel();
        const patch = 'foobar';
        const completer = new Completer({ editor: null, model });
        const handler = new TestCompletionHandler({ completer, connector });
        const editor = createEditorWidget().editor;
        const text = 'eggs\nfoo # comment\nbaz';
        const want = 'eggs\nfoobar # comment\nbaz';
        const line = 1;
        const column = 5;
        const request: Completer.ITextState = {
          column,
          line,
          lineHeight: 0,
          charWidth: 0,
          coords: null,
          text
        };

        handler.editor = editor;
        handler.editor.model.value.text = text;
        handler.editor.setCursorPosition({ line, column: column + 3 });
        model.original = request;
        model.cursor = { start: column, end: column + 3 };
        // Make the completion, check its value and cursor position.
        (completer.selected as any).emit(patch);
        expect(editor.model.value.text).toBe(want);
        expect(editor.getCursorPosition()).toEqual({
          line,
          column: column + 6
        });
        console.warn(editor.getCursorPosition());
        // Undo the completion, check its value and cursor position.
        editor.undo();
        expect(editor.model.value.text).toBe(text);
        expect(editor.getCursorPosition()).toEqual({
          line,
          column: column + 3
        });
        console.warn(editor.getCursorPosition());
        // Redo the completion, check its value and cursor position.
        editor.redo();
        expect(editor.model.value.text).toBe(want);
        expect(editor.getCursorPosition()).toEqual({
          line,
          column: column + 6
        });
        console.warn(editor.getCursorPosition());
      });
    });
  });
});
