import '../fixtures/window';
import { Editor, globalContext, Setters } from '@felce/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import { Designer } from '../../src/designer/designer';
import { Dragon } from '../../src/designer/dragon';
// import { TransformStage } from '../../src/document/node/transform-stage';
import formSchema from '../fixtures/schema/form';
import buttonMetadata from '../fixtures/component-metadata/button';
import pageMetadata from '../fixtures/component-metadata/page';
import divMetadata from '../fixtures/component-metadata/div';
import { delayObxTick } from '../utils';
import { fireEvent } from '@testing-library/react';
import { IPublicEnumDragObjectType, IPublicEnumTransformStage } from '@felce/lowcode-types';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';

const mockNode = {
  internalToShellNode() {
    return 'mockNode';
  },
};

describe('Designer 测试', () => {
  let editor: Editor;
  let designer: Designer;
  let project: Project;
  let doc: DocumentModel;
  let dragon: Dragon;

  beforeAll(() => {
    editor = new Editor();
    const setters = new Setters();
    editor.set('setters', setters);
    !globalContext.has(Editor) && globalContext.register(editor, Editor);
  });

  beforeEach(() => {
    designer = new Designer({ editor, shellModelFactory });
    project = designer.project;
    doc = project.createDocument(formSchema);
    dragon = new Dragon(designer);
  });

  afterEach(() => {
    project.unload();
    project.mountSimulator(undefined);
    designer.purge();
    designer = null;
    project = null;
    dragon = null;
  });

  describe('onDragstart / onDrag / onDragend', () => {
    it('IPublicEnumDragObjectType.Node', () => {
      const dragStartMockFn = vi.fn();
      const dragMockFn = vi.fn();
      const dragEndMockFn = vi.fn();
      const dragStartMockFn2 = vi.fn();
      const dragMockFn2 = vi.fn();
      const dragEndMockFn2 = vi.fn();

      const designer = new Designer({
        editor,
        shellModelFactory,
        onDragstart: dragStartMockFn,
        onDrag: dragMockFn,
        onDragend: dragEndMockFn,
      });
      editor.on('designer.dragstart', dragStartMockFn2);
      editor.on('designer.drag', dragMockFn2);
      editor.on('designer.dragend', dragEndMockFn2);
      const { dragon } = designer;

      dragon.boost(
        {
          type: IPublicEnumDragObjectType.Node,
          nodes: [doc.getNode('node_k1ow3cbn')],
        },
        new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
      );

      fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
      expect(dragStartMockFn).toHaveBeenCalledTimes(1);
      expect(dragStartMockFn2).toHaveBeenCalledTimes(1);
      expect(dragMockFn).toHaveBeenCalledTimes(1);
      expect(dragMockFn2).toHaveBeenCalledTimes(1);

      fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
      expect(dragMockFn).toHaveBeenCalledTimes(2);
      expect(dragMockFn2).toHaveBeenCalledTimes(2);

      setMockDropLocation();
      fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });

      expect(dragEndMockFn).toHaveBeenCalledTimes(1);
      expect(dragEndMockFn2).toHaveBeenCalledTimes(1);

      function setMockDropLocation() {
        const mockTarget = {
          document: doc,
          children: {
            get(x) {
              return x;
            },
            insert() {},
            internalInsert() {},
          },
        };
        const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
        const mockSource = {};
        const mockEvent = { type: 'LocateEvent', nodes: [] };

        return designer.createLocation({
          target: mockTarget,
          detail: mockDetail,
          source: mockSource,
          event: mockEvent,
        });
      }
    });

    it('IPublicEnumDragObjectType.NodeData', () => {
      const dragStartMockFn = vi.fn();
      const dragMockFn = vi.fn();
      const dragEndMockFn = vi.fn();
      const dragStartMockFn2 = vi.fn();
      const dragMockFn2 = vi.fn();
      const dragEndMockFn2 = vi.fn();

      const designer = new Designer({
        editor,
        shellModelFactory,
        onDragstart: dragStartMockFn,
        onDrag: dragMockFn,
        onDragend: dragEndMockFn,
      });
      editor.on('designer.dragstart', dragStartMockFn2);
      editor.on('designer.drag', dragMockFn2);
      editor.on('designer.dragend', dragEndMockFn2);
      const { dragon } = designer;

      dragon.boost(
        {
          type: IPublicEnumDragObjectType.NodeData,
          data: [
            {
              componentName: 'Button',
            },
          ],
        },
        new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
      );

      fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
      expect(dragStartMockFn).toHaveBeenCalledTimes(1);
      expect(dragStartMockFn2).toHaveBeenCalledTimes(1);
      expect(dragMockFn).toHaveBeenCalledTimes(1);
      expect(dragMockFn2).toHaveBeenCalledTimes(1);

      fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
      expect(dragMockFn).toHaveBeenCalledTimes(2);
      expect(dragMockFn2).toHaveBeenCalledTimes(2);

      setMockDropLocation();
      fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });

      expect(dragEndMockFn).toHaveBeenCalledTimes(1);
      expect(dragEndMockFn2).toHaveBeenCalledTimes(1);

      function setMockDropLocation() {
        const mockTarget = {
          document: doc,
          children: {
            get(x) {
              return x;
            },
            insert() {},
            internalInsert() {},
          },
        };
        const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
        const mockSource = {};
        const mockEvent = { type: 'LocateEvent', nodes: [] };

        return designer.createLocation({
          target: mockTarget,
          detail: mockDetail,
          source: mockSource,
          event: mockEvent,
        });
      }
    });
  });

  it('addPropsReducer / transformProps', () => {
    // 没有相应的 reducer
    expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({
      num: 1,
    });
    // props 是数组
    expect(designer.transformProps([{ num: 1 }], mockNode, IPublicEnumTransformStage.Init)).toEqual(
      [{ num: 1 }],
    );

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Init);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Init);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Clone);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Serilize);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Render);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Save);

    designer.addPropsReducer((props, node) => {
      props.num += 1;
      return props;
    }, IPublicEnumTransformStage.Upgrade);

    expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({
      num: 3,
    });
    expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Clone)).toEqual({
      num: 2,
    });
    expect(
      designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Serilize),
    ).toEqual({ num: 2 });
    expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Render)).toEqual(
      { num: 2 },
    );
    expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Save)).toEqual({
      num: 2,
    });
    expect(
      designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade),
    ).toEqual({ num: 2 });

    designer.addPropsReducer((props, node) => {
      throw new Error('calculate error');
    }, IPublicEnumTransformStage.Upgrade);
    expect(
      designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade),
    ).toEqual({ num: 2 });
  });

  it('setProps', () => {
    // 第一次设置 props
    const initialProps = {
      simulatorComponent: { isSimulatorComp: true },
      simulatorProps: { designMode: 'design' },
      suspensed: true,
      componentMetadatas: [buttonMetadata, divMetadata],
    };
    designer = new Designer({
      editor,
      shellModelFactory,
      ...initialProps,
    });

    expect(designer.simulatorComponent).toEqual({ isSimulatorComp: true });
    expect(designer.simulatorProps).toEqual({ designMode: 'design' });
    expect(designer.suspensed).toBeTruthy();
    expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
    expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
    const {
      editor: editorFromDesigner,
      shellModelFactory: shellModelFactoryFromDesigner,
      ...others
    } = (designer as any).props;
    expect(others).toEqual(initialProps);
    expect(designer.get('simulatorProps')).toEqual({ designMode: 'design' });
    expect(designer.get('suspensed')).toBeTruthy();
    expect(designer.get('xxx')).toBeUndefined();

    // 第二次设置 props
    const updatedProps = {
      simulatorComponent: { isSimulatorComp2: true },
      simulatorProps: { designMode: 'live' },
      suspensed: false,
      componentMetadatas: [buttonMetadata],
    };
    designer.setProps(updatedProps);

    expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true });
    expect(designer.simulatorProps).toEqual({ designMode: 'live' });
    expect(designer.suspensed).toBeFalsy();
    expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
    expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
    const {
      editor: editorFromDesigner2,
      shellModelFactory: shellModelFactoryFromDesigner2,
      ...others2
    } = (designer as any).props;
    expect(others2).toEqual(updatedProps);

    // 第三次设置 props，跟第二次值一样，for 覆盖率测试
    const updatedProps2 = updatedProps;
    designer.setProps(updatedProps2);

    expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true });
    expect(designer.simulatorProps).toEqual({ designMode: 'live' });
    expect(designer.suspensed).toBeFalsy();
    expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
    expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
    const {
      editor: editorFromDesigner3,
      shellModelFactory: shellModelFactoryFromDesigner3,
      ...others3
    } = (designer as any).props;
    expect(others3).toEqual(updatedProps);
  });

  describe('getSuitableInsertion', () => {
    it('没有 currentDocument', () => {
      project.unload();
      expect(designer.getSuitableInsertion({})).toBeNull();
    });

    it('有选中节点，isContainer && 允许放子节点', () => {
      designer.createComponentMeta(divMetadata);
      designer.createComponentMeta(buttonMetadata);
      designer.currentSelection?.select('node_k1ow3cbo');
      const { target, index } = designer.getSuitableInsertion(
        doc.createNode({ componentName: 'Button' }),
      );
      expect(target).toBe(doc.getNode('node_k1ow3cbo'));
      expect(index).toBeUndefined();
    });

    it('有选中节点，不是 isContainer', () => {
      designer.createComponentMeta(divMetadata);
      designer.createComponentMeta(buttonMetadata);
      designer.currentSelection?.select('node_k1ow3cbn');
      const { target, index } = designer.getSuitableInsertion(
        doc.createNode({ componentName: 'Button' }),
      );
      expect(target).toBe(doc.getNode('node_k1ow3cbo'));
      expect(index).toBe(1);
    });

    it('无选中节点', () => {
      designer.createComponentMeta(pageMetadata);
      const { target, index } = designer.getSuitableInsertion(
        doc.createNode({ componentName: 'Button' }),
      );
      expect(target).toBe(doc.getNode('page'));
      expect(index).toBeUndefined();
    });
  });

  it('getComponentMetasMap', () => {
    designer.createComponentMeta({
      componentName: 'Div',
      title: '容器',
      docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
      devMode: 'procode',
      tags: ['布局'],
    });

    expect(designer.getComponentMetasMap().get('Div')).not.toBeUndefined();
  });

  it('refreshComponentMetasMap', () => {
    designer.createComponentMeta({
      componentName: 'Div',
      title: '容器',
      docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
      devMode: 'procode',
      tags: ['布局'],
    });

    const originalMetasMap = designer.getComponentMetasMap();
    designer.refreshComponentMetasMap();

    expect(originalMetasMap).not.toBe(designer.getComponentMetasMap());
  });

  describe('loadIncrementalAssets', () => {
    it('components && packages', async () => {
      editor.set('assets', { components: [], packages: [] });
      const fn = vi.fn();

      project.mountSimulator({
        setupComponents: fn,
      });
      await designer.loadIncrementalAssets({
        components: [
          {
            componentName: 'Div2',
            title: '容器',
            docUrl:
              'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
            devMode: 'proCode',
            tags: ['布局'],
          },
        ],
        packages: [],
      });

      const comps = editor.get('assets').components;
      expect(comps).toHaveLength(1);
      expect(fn).toHaveBeenCalled();
    });

    it('no components && packages', async () => {
      editor.set('assets', { components: [], packages: [] });
      const fn = vi.fn();

      project.mountSimulator({
        setupComponents: fn,
      });
      await designer.loadIncrementalAssets({});

      expect(fn).not.toHaveBeenCalled();
    });
  });

  it('createLocation / clearLocation', () => {
    const mockTarget = {
      document: doc,
      children: {
        get(x) {
          return x;
        },
        insert() {},
        internalInsert() {},
      },
    };
    const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
    const mockSource = {};
    const mockEvent = { type: 'LocateEvent', nodes: [] };

    const loc = designer.createLocation({
      target: mockTarget,
      detail: mockDetail,
      source: mockSource,
      event: mockEvent,
    });

    expect(designer.dropLocation).toBe(loc);

    const doc2 = project.createDocument({ componentName: 'Page' });
    designer.createLocation({
      target: {
        document: doc2,
        children: {
          get(x) {
            return x;
          },
          insert() {},
          internalInsert() {},
        },
      },
      detail: mockDetail,
      source: mockSource,
      event: mockEvent,
    });

    designer.clearLocation();
    expect(designer.dropLocation).toBeUndefined();
  });

  it('autorun', async () => {
    const mockFn = vi.fn();
    designer.autorun(() => {
      mockFn();
    }, true);

    await delayObxTick();

    expect(mockFn).toHaveBeenCalled();
  });

  it('suspensed', () => {
    designer.suspensed = true;
    expect(designer.suspensed).toBeTruthy();
    designer.suspensed = false;
    expect(designer.suspensed).toBeFalsy();
  });

  it('schema', () => {
    // TODO: matchSnapshot
    designer.schema;
    designer.setSchema({
      componentsTree: [
        {
          componentName: 'Page',
          props: {},
        },
      ],
    });
  });

  it('createOffsetObserver / clearOobxList / touchOffsetObserver', () => {
    project.mountSimulator({
      computeComponentInstanceRect() {},
    });
    designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
    expect(designer.oobxList).toHaveLength(1);
    designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
    expect(designer.oobxList).toHaveLength(2);

    designer.clearOobxList(true);
    expect(designer.oobxList).toHaveLength(0);

    const obx = designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
    obx.pid = 'xxx';
    obx.compute = () => {};
    expect(designer.oobxList).toHaveLength(1);

    designer.touchOffsetObserver();
    expect(designer.oobxList).toHaveLength(1);
  });
});
