import { CorePreferences, IContextKeyService, PreferenceService } from '@opensumi/ide-core-browser';
import { Deferred, Disposable, IEventBus, URI, createContributionProvider } from '@opensumi/ide-core-common';
import {
  BrowserEditorContribution,
  CodeEditorDidVisibleEvent,
  EditorComponentRegistry,
  EditorGroupChangeEvent,
  EditorOpenType,
  EmptyDocCacheImpl,
  IEditorDecorationCollectionService,
  IEditorDocumentModelContentRegistry,
  IEditorDocumentModelService,
  IEditorFeatureRegistry,
  ResourceOpenTypeChangedEvent,
} from '@opensumi/ide-editor/lib/browser';
import { EditorComponentRegistryImpl } from '@opensumi/ide-editor/lib/browser/component';
import { isEOLStack, isEditStack } from '@opensumi/ide-editor/lib/browser/doc-model/editor-is-fn';
import {
  EditorDocumentModelContentRegistryImpl,
  EditorDocumentModelServiceImpl,
  SaveTask,
} from '@opensumi/ide-editor/lib/browser/doc-model/main';
import { EditorCollectionServiceImpl } from '@opensumi/ide-editor/lib/browser/editor-collection.service';
import { EditorDecorationCollectionService } from '@opensumi/ide-editor/lib/browser/editor.decoration.service';
import { EditorFeatureRegistryImpl } from '@opensumi/ide-editor/lib/browser/feature';
import { LanguageService } from '@opensumi/ide-editor/lib/browser/language/language.service';
import { ResourceServiceImpl } from '@opensumi/ide-editor/lib/browser/resource.service';
import { EditorGroup, WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
import {
  EditorCollectionService,
  EditorGroupSplitAction,
  ILanguageService,
  ResourceService,
  WorkbenchEditorService,
} from '@opensumi/ide-editor/lib/common';
import { IDocPersistentCacheProvider } from '@opensumi/ide-editor/lib/common';
import { MonacoService } from '@opensumi/ide-monaco';
import { IMessageService } from '@opensumi/ide-overlay';
import { IWorkspaceService } from '@opensumi/ide-workspace';
import { MockWorkspaceService } from '@opensumi/ide-workspace/lib/common/mocks';
import { IConfigurationService } from '@opensumi/monaco-editor-core/esm/vs/platform/configuration/common/configuration';

import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { useMockStorage } from '../../../core-browser/__mocks__/storage';
import { MockContextKeyService } from '../../../monaco/__mocks__/monaco.context-key.service';
import { MockedMonacoService } from '../../../monaco/__mocks__/monaco.service.mock';

import {
  TestEditorDocumentProvider,
  TestResourceComponent,
  TestResourceProvider,
  TestResourceResolver,
  TestResourceResolver2,
  doNotClose,
} from './test-providers';

const injector = createBrowserInjector([]);

injector.addProviders(
  ...[
    {
      token: EditorCollectionService,
      useClass: EditorCollectionServiceImpl,
    },
    {
      token: WorkbenchEditorService,
      useClass: WorkbenchEditorServiceImpl,
    },
    {
      token: ResourceService,
      useClass: ResourceServiceImpl,
    },
    {
      token: EditorComponentRegistry,
      useClass: EditorComponentRegistryImpl,
    },
    {
      token: IEditorDecorationCollectionService,
      useClass: EditorDecorationCollectionService,
    },
    {
      token: IEditorDocumentModelContentRegistry,
      useClass: EditorDocumentModelContentRegistryImpl,
    },
    {
      token: IEditorDocumentModelService,
      useClass: EditorDocumentModelServiceImpl,
    },
    {
      token: ILanguageService,
      useClass: LanguageService,
    },
    {
      token: MonacoService,
      useClass: MockedMonacoService,
    },
    {
      token: IWorkspaceService,
      useClass: MockWorkspaceService,
    },
    {
      token: IDocPersistentCacheProvider,
      useClass: EmptyDocCacheImpl,
    },
    {
      token: IEditorFeatureRegistry,
      useClass: EditorFeatureRegistryImpl,
    },
    {
      token: IContextKeyService,
      useClass: MockContextKeyService,
    },
    {
      token: IMessageService,
      useValue: {},
    },
  ],
);
useMockStorage(injector);
injector.overrideProviders({
  token: CorePreferences,
  useValue: {
    'editor.previewMode': true,
  },
});
injector.overrideProviders({
  token: IConfigurationService,
  useValue: {
    getValue() {
      return true;
    },
    onDidChangeConfiguration() {
      return new Disposable();
    },
  },
});
injector.overrideProviders({
  token: PreferenceService,
  useValue: {
    get() {
      return true;
    },
    onPreferenceChanged() {
      return new Disposable();
    },
    onPreferencesChanged() {
      return new Disposable();
    },
  },
});
createContributionProvider(injector, BrowserEditorContribution);

describe('editor collection service tests', () => {
  it('should be able to create and dispose editors', async () => {
    const editorService: EditorCollectionService = injector.get(EditorCollectionService);
    const editor = await editorService.createCodeEditor(document.createElement('div'));
    expect(editor).toBeDefined();

    expect(editorService.listEditors().length).toBe(1);

    const diffEditor = await editorService.createDiffEditor(document.createElement('div'));
    expect(diffEditor).toBeDefined();

    expect(editorService.listEditors().length).toBe(3);
    expect(editorService.listDiffEditors().length).toBe(1);

    editor.dispose();
    diffEditor.dispose();

    expect(editorService.listEditors().length).toBe(0);
    expect(editorService.listDiffEditors().length).toBe(0);
  });
});

describe('workbench editor service tests', () => {
  // prepare
  const editorService: WorkbenchEditorService = injector.get(WorkbenchEditorService);

  const resourceService: ResourceService = injector.get(ResourceService);
  const editorComponentRegistry: EditorComponentRegistry = injector.get(EditorComponentRegistry);
  const editorDocModelRegistry: IEditorDocumentModelContentRegistry = injector.get(IEditorDocumentModelContentRegistry);
  const eventBus: IEventBus = injector.get(IEventBus);

  const disposer = new Disposable();
  beforeAll(() => {
    injector.mockCommand('explorer.location');
    const globalContextKeyService: IContextKeyService = injector.get(IContextKeyService);
    const editorContextKeyService = globalContextKeyService.createScoped();
    editorService.setEditorContextKeyService(editorContextKeyService);
    (editorService as unknown as WorkbenchEditorServiceImpl).prepareContextKeyService();
    disposer.addDispose(resourceService.registerResourceProvider(TestResourceProvider));
    disposer.addDispose(editorComponentRegistry.registerEditorComponent(TestResourceComponent));
    disposer.addDispose(editorComponentRegistry.registerEditorComponentResolver('test', TestResourceResolver));
    disposer.addDispose(editorComponentRegistry.registerEditorComponentResolver('test', TestResourceResolver2));
    disposer.addDispose(editorDocModelRegistry.registerEditorDocumentModelContentProvider(TestEditorDocumentProvider));
    (editorService as any).contributionsReady.resolve();
    editorService.onDidEditorGroupsChanged(() => {
      editorService.editorGroups.forEach((g) => {
        if (!g.codeEditor) {
          (g as EditorGroup).createEditor(document.createElement('div'));
          (g as EditorGroup).createDiffEditor(document.createElement('div'));
        }
      });
    });
  });

  it('should be able to open uri', async () => {
    const testCodeUri = new URI('test://testUri1');
    const listener = jest.fn();
    const disposer = (editorService.currentEditorGroup as EditorGroup).onDidEditorGroupTabChanged(listener);

    await editorService.open(testCodeUri);

    expect(editorService.currentResource).toBeDefined();
    expect(editorService.currentResource!.uri.toString()).toBe(testCodeUri.toString());
    expect(listener).toHaveBeenCalled();

    await editorService.closeAll();
    disposer.dispose();
  });

  it('should be able to fire loading state for big resources', async () => {
    expect.assertions(2);
    const listener = jest.fn();
    const testLoadingCodeUri = new URI('test://test/loading');
    const testCodeUri = new URI('test://testUri1');
    const defered = new Deferred();
    const disposer = editorService.currentEditorGroup.onDidEditorGroupContentLoading(async (resource) => {
      listener();
      const status = editorService.currentEditorGroup.resourceStatus.get(resource);
      expect(status).toBeDefined();
      await status?.finally(async () => {
        disposer.dispose();
        await editorService.closeAll();
        defered.resolve();
      });
    });

    await editorService.open(testCodeUri);
    await editorService.open(testLoadingCodeUri);
    expect(listener).toHaveBeenCalledTimes(1);
    await defered.promise;
  });

  it('should keep resource status and emit loading when reopening with new type takes time', async () => {
    jest.useFakeTimers();
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const group = editorService.currentEditorGroup as EditorGroup;

    const displayDeferred = new Deferred<void>();
    const displaySpy = jest
      .spyOn(group as any, 'displayResourceComponent')
      .mockImplementation(async () => displayDeferred.promise);
    const loadingSpy = jest.spyOn(group as any, 'notifyTabLoading');

    try {
      eventBus.fire(new ResourceOpenTypeChangedEvent(testCodeUri));
      await Promise.resolve();

      const status = group.resourceStatus.get(group.currentResource!);
      expect(status).toBeInstanceOf(Promise);

      jest.advanceTimersByTime(80);
      expect(loadingSpy).toHaveBeenCalledTimes(1);

      displayDeferred.resolve();
      await status;

      expect(displaySpy).toHaveBeenCalled();
    } finally {
      displaySpy.mockRestore();
      loadingSpy.mockRestore();
      jest.useRealTimers();
      await editorService.closeAll();
    }
  });

  it('should clear delayed loading when open type change resolves quickly', async () => {
    jest.useFakeTimers();
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const group = editorService.currentEditorGroup as EditorGroup;

    const displaySpy = jest
      .spyOn(group as any, 'displayResourceComponent')
      .mockImplementation(async () => Promise.resolve());
    const loadingSpy = jest.spyOn(group as any, 'notifyTabLoading');

    try {
      eventBus.fire(new ResourceOpenTypeChangedEvent(testCodeUri));
      await Promise.resolve();

      const status = group.resourceStatus.get(group.currentResource!);
      await status;

      jest.runOnlyPendingTimers();
      expect(loadingSpy).not.toHaveBeenCalled();
    } finally {
      displaySpy.mockRestore();
      loadingSpy.mockRestore();
      jest.useRealTimers();
      await editorService.closeAll();
    }
  });

  it('should be able to open component', async () => {
    const testComponentUri = new URI('test://component');
    const listener = jest.fn();
    const disposer = (editorService.currentEditorGroup as EditorGroup).onDidEditorGroupBodyChanged(listener);

    await editorService.open(testComponentUri);
    expect(editorService.editorGroups[0].currentOpenType).toBeDefined();
    expect(editorService.editorGroups[0].currentOpenType!.type).toBe(EditorOpenType.component);
    expect(listener).toHaveBeenCalled();

    await editorService.closeAll();

    await editorService.open(testComponentUri, { preview: false, forceOpenType: { type: EditorOpenType.code } });
    expect(editorService.editorGroups[0].currentOpenType).toBeDefined();
    expect(editorService.editorGroups[0].currentOpenType!.type).toBe(EditorOpenType.code);

    // 测试 getState 方法
    expect(editorService.editorGroups[0].getState()).toEqual({
      uris: ['test://component'],
      current: 'test://component',
      previewIndex: -1,
    });

    await editorService.closeAll();

    disposer.dispose();
  });

  it('should be able to split', async () => {
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri);
    await editorService.open(testCodeUri, { split: EditorGroupSplitAction.Right });
    await editorService.open(testCodeUri, { split: EditorGroupSplitAction.Bottom });
    expect(editorService.editorGroups.length).toBe(3);

    await editorService.closeAll();
  });

  it('should focus editor', async () => {
    const testCodeUri = new URI('test:///testuri1');
    const focused = jest.fn();
    editorService.currentEditorGroup.codeEditor.monacoEditor.onDidFocusEditorText(focused);
    await editorService.open(testCodeUri, { focus: true });
    eventBus.fire(
      new CodeEditorDidVisibleEvent({
        groupName: editorService.currentEditorGroup.name,
        editorId: editorService.currentEditorGroup.codeEditor.getId(),
        type: EditorOpenType.code,
      }),
    );

    expect(focused).toHaveBeenCalled();

    await editorService.closeAll();
  });

  it('preview mode should work', async () => {
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri, { preview: true });
    const testCodeUri2 = new URI('test://testUri2');
    await editorService.open(testCodeUri2, { preview: true });
    expect(editorService.editorGroups[0].resources.length).toBe(1);

    await editorService.closeAll();
  });

  it('pined mode should work', async () => {
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const testCodeUri2 = new URI('test://testUri2');
    await editorService.open(testCodeUri2, { preview: false });
    expect(editorService.editorGroups[0].resources.length).toBe(2);

    await editorService.closeAll();
  });

  it('pined uri should be empty after close all', async () => {
    const testCodeUri = new URI('test://testUri1');
    await editorService.open(testCodeUri, { preview: true });
    await editorService.closeAll();
    expect((editorService as WorkbenchEditorServiceImpl).currentEditorGroup.previewURI).toBeNull();
    await editorService.open(testCodeUri, { preview: true });
    expect(editorService.editorGroups[0].resources.length).toBe(1);

    await editorService.closeAll();
  });

  it('replace should work properly', async () => {
    const testCodeUri = new URI('test://a/testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const testCodeUri2 = new URI('test://a/testUri2');
    await editorService.open(testCodeUri2, { preview: false });
    const testCodeUri3 = new URI('test://a/testUri3');
    await editorService.open(testCodeUri3, { preview: false });

    await editorService.open(testCodeUri2, { preview: false });

    const testCodeUri4 = new URI('test://a/testUri4');
    await editorService.open(testCodeUri4, { preview: false, replace: true });

    expect(editorService.currentEditorGroup.resources.map((r) => r.uri.toString())).toEqual([
      'test://a/testUri1',
      'test://a/testUri4',
      'test://a/testUri3',
    ]);

    await editorService.open(testCodeUri2, { preview: false, replace: true, index: 0 });

    expect(editorService.currentEditorGroup.resources.map((r) => r.uri.toString())).toEqual([
      'test://a/testUri2',
      'test://a/testUri4',
      'test://a/testUri3',
    ]);

    // 不允许关闭的情况
    doNotClose.push(testCodeUri4.toString());

    const testCodeUri5 = new URI('test://a/testUri5');
    await editorService.open(testCodeUri5, { preview: false, replace: true, index: 1 });

    expect(editorService.currentEditorGroup.resources.map((r) => r.uri.toString())).toEqual([
      'test://a/testUri2',
      'test://a/testUri5',
      'test://a/testUri4',
      'test://a/testUri3',
    ]);

    doNotClose.splice(0, doNotClose.length);
    await editorService.closeAll();
  });

  it('closeOthers should notify tab changed', async () => {
    const testCodeUri = new URI('test://a/testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const testCodeUri2 = new URI('test://a/testUri2');
    await editorService.open(testCodeUri2, { preview: false });
    const testCodeUri3 = new URI('test://a/testUri3');
    await editorService.open(testCodeUri3, { preview: false });

    const listener = jest.fn();
    const disposer = (editorService.currentEditorGroup as EditorGroup).onDidEditorGroupTabChanged(listener);

    await (editorService.currentEditorGroup as EditorGroup).closeOthers(testCodeUri2);

    expect(listener).toHaveBeenCalled();

    await editorService.closeAll();
    disposer.dispose();
  });

  it('close all tabs should emit EditorGroupChangeEvent', async () => {
    const testCodeUri = new URI('test://a/testUri1');
    await editorService.open(testCodeUri, { preview: false });
    const testCodeUri2 = new URI('test://a/testUri2');
    await editorService.open(testCodeUri2, { preview: false });
    const testCodeUri3 = new URI('test://a/testUri3');
    await editorService.open(testCodeUri3, { preview: false });

    const eventBus = injector.get(IEventBus);

    const listener = jest.fn();
    const disposer = eventBus.on(EditorGroupChangeEvent, listener);

    await editorService.closeAll();

    expect(listener).toHaveBeenCalledWith(
      expect.objectContaining({
        payload: expect.objectContaining({
          newResource: null,
          oldResource: expect.anything(),
        }),
      }),
    );
    disposer.dispose();
  });

  it('close last tabs should emit EditorGroupChangeEvent', async () => {
    const testCodeUri = new URI('test://a/testUri1');
    await editorService.open(testCodeUri, { preview: false });

    const eventBus = injector.get(IEventBus);

    const listener = jest.fn();
    const disposer = eventBus.on(EditorGroupChangeEvent, listener);

    await editorService.close(testCodeUri);

    expect(listener).toHaveBeenCalledWith(
      expect.objectContaining({
        payload: expect.objectContaining({
          newResource: null,
          oldResource: expect.anything(),
        }),
      }),
    );
    disposer.dispose();
  });

  it('side widget registration should be ok', () => {
    editorComponentRegistry.registerEditorSideWidget({
      component: () => null as any,
      id: 'test-1',
      displaysOnResource: (resource) => resource.uri.scheme === 'testScheme',
    });

    expect(editorComponentRegistry.getSideWidgets('bottom', { uri: new URI('testScheme://tes/t') } as any).length).toBe(
      1,
    );
    expect(
      editorComponentRegistry.getSideWidgets('bottom', { uri: new URI('testScheme2://tes/t') } as any).length,
    ).toBe(0);
  });

  afterAll(() => {
    disposer.dispose();
  });
});

describe('utils test', () => {
  it('util tests', () => {
    expect(isEditStack({ editOperations: [] } as any)).toBeTruthy();
    expect(isEditStack({} as any)).toBeFalsy();

    expect(isEOLStack({ eol: [] } as any)).toBeTruthy();
    expect(isEOLStack({} as any)).toBeFalsy();
  });

  it('save task', async () => {
    const service: any = {
      saveEditorDocumentModel: jest.fn((uri, content) => {
        if (content.indexOf('fail') > -1) {
          throw new Error('test fail');
        } else {
          return {
            state: 'success',
          };
        }
      }),
    };
    const saveTask1 = new SaveTask(new URI('file:///test/test.js'), 1, 1, 'test success', true);

    const res1 = await saveTask1.run(service, 'test begin', []);

    expect(res1.state).toBe('success');

    const saveTask2 = new SaveTask(new URI('file:///test/test.js'), 1, 1, 'test fail', true);
    const res2 = await saveTask2.run(service, 'test begin', []);

    expect(res2.state).toBe('error');
  });
});
