import '../../fixtures/window';
import { mobx, makeAutoObservable, globalContext, Editor } from '@alilc/lowcode-editor-core';
import { History } from '../../../src/document/history';
import { delay } from '../../utils/misc';
import { Workspace } from '@alilc/lowcode-workspace';

class Node {
  data: number;
  children: Node[] = [];

  constructor(data: number) {
    makeAutoObservable(this);
    this.data = data;
  }

  addNode(node: Node) {
    this.children.push(node);
  }

  toObject() {
    return {
      data: this.data,
      children: this.children.map((c) => c.toObject()),
    };
  }
}

let tree: Node = null;
beforeEach(() => {
  tree = new Node(1);
  tree.addNode(new Node(2));
});

afterEach(() => {
  tree = null;
});

describe('History', () => {
  beforeAll(() => {
    const editor = new Editor();
    globalContext.register(editor, Editor);
    globalContext.register(editor, 'editor');
    globalContext.register(new Workspace(), 'workspace');
  });

  it('data function & records', async () => {
    const mockRedoFn = jest.fn();
    const mockDataFn = jest.fn();
    const history = new History<Node>(() => {
      const data = tree.toObject();
      mockDataFn(data);
      return data;
    }, mockRedoFn);

    expect(mockDataFn).toHaveBeenCalledTimes(1);
    expect(mockDataFn).toHaveBeenCalledWith({ data: 1, children: [{ data: 2, children: [] }] });
    expect(history.hotData).toMatchSnapshot();
    // @ts-ignore
    expect(history.session.cursor).toBe(0);
    // @ts-ignore
    expect(history.records).toHaveLength(1);

    tree.data = 3;
    expect(mockDataFn).toHaveBeenCalledTimes(2);
    expect(mockDataFn).toHaveBeenCalledWith({ data: 3, children: [{ data: 2, children: [] }] });
    expect(history.hotData).toMatchSnapshot();
    // @ts-ignore
    expect(history.session.cursor).toBe(0);
    // @ts-ignore
    expect(history.records).toHaveLength(1);

    // modify data after timeGap
    await delay(1200);
    tree.data = 5;
    expect(mockDataFn).toHaveBeenCalledTimes(3);
    expect(mockDataFn).toHaveBeenCalledWith({ data: 5, children: [{ data: 2, children: [] }] });
    expect(history.hotData).toMatchSnapshot();
    // @ts-ignore
    expect(history.session.cursor).toBe(1);
    // @ts-ignore
    expect(history.records).toHaveLength(2);

    history.setSerialization({
      serialize(data: Node): string {
        return JSON.stringify(data);
      },
      unserialize(data: string) {
        return JSON.parse(data);
      },
    });

    // modify data after timeGap
    await delay(1200);
    tree.data = 7;
    expect(mockDataFn).toHaveBeenCalledTimes(4);
    expect(mockDataFn).toHaveBeenCalledWith({ data: 7, children: [{ data: 2, children: [] }] });
    expect(history.hotData).toMatchSnapshot();
  });

  it('isSavePoint & savePoint', async () => {
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      () => {},
    );

    expect(history.isSavePoint()).toBeFalsy();
    expect(history.isModified()).toBeFalsy();

    await delay(1200);
    tree.data = 3;
    expect(history.isSavePoint()).toBeTruthy();

    history.savePoint();
    expect(history.isSavePoint()).toBeFalsy();
  });

  it('go & forward & back & onCursor', async () => {
    const mockRedoFn = jest.fn();
    const mockCursorFn = jest.fn();
    const mockStateFn = jest.fn();
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
        mockRedoFn(data);
      },
    );

    // undoable ❌ & redoable ❌ & modified ❌
    expect(history.getState()).toBe(0);

    await delay(1200);
    tree.data = 3;

    await delay(1200);
    tree.data = 5;

    await delay(1200);
    tree.data = 7;

    const dataCursor0 = { data: 1, children: [{ data: 2, children: [] }] };
    const dataCursor1 = { data: 3, children: [{ data: 2, children: [] }] };
    const dataCursor2 = { data: 5, children: [{ data: 2, children: [] }] };
    const dataCursor3 = { data: 7, children: [{ data: 2, children: [] }] };

    // redoable ❌
    expect(history.getState()).toBe(7 - 2);

    const off1 = history.onCursor(mockCursorFn);
    const off2 = history.onStateChange(mockStateFn);

    // @ts-ignore
    expect(history.records).toHaveLength(4);
    // @ts-ignore
    expect(history.session.cursor).toBe(3);

    // step 1
    history.back();
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      1,
      JSON.stringify(dataCursor2),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(1, 7);
    expect(mockRedoFn).toHaveBeenNthCalledWith(1, dataCursor2);

    // step 2
    history.back();
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      2,
      JSON.stringify(dataCursor1),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(2, 7);
    expect(mockRedoFn).toHaveBeenNthCalledWith(2, dataCursor1);

    // step 3
    history.back();
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      3,
      JSON.stringify(dataCursor0),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(3, 7 - 4 - 1);
    expect(mockRedoFn).toHaveBeenNthCalledWith(3, dataCursor0);

    // step 4
    history.forward();
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      4,
      JSON.stringify(dataCursor1),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(4, 7);
    expect(mockRedoFn).toHaveBeenNthCalledWith(4, dataCursor1);

    // step 5
    history.forward();
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      5,
      JSON.stringify(dataCursor2),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(5, 7);
    expect(mockRedoFn).toHaveBeenNthCalledWith(5, dataCursor2);

    // step 6
    history.go(3);
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      6,
      JSON.stringify(dataCursor3),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(6, 7 - 2);
    expect(mockRedoFn).toHaveBeenNthCalledWith(6, dataCursor3);

    // step 7
    history.go(0);
    expect(mockCursorFn).toHaveBeenNthCalledWith(
      7,
      JSON.stringify(dataCursor0),
    );
    expect(mockStateFn).toHaveBeenNthCalledWith(7, 7 - 4 - 1);
    expect(mockRedoFn).toHaveBeenNthCalledWith(7, dataCursor0);

    off1();
    off2();
    mockStateFn.mockClear();
    mockCursorFn.mockClear();
    history.go(1);
    expect(mockStateFn).not.toHaveBeenCalled();
    expect(mockCursorFn).not.toHaveBeenCalled();
  });

  it('go() - edge case of cursor', async () => {
    const mockRedoFn = jest.fn();
    const mockCursorFn = jest.fn();
    const mockStateFn = jest.fn();
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
        mockRedoFn(data);
      },
    );

    await delay(1200);
    tree.data = 3;

    await delay(1200);
    tree.data = 5;

    history.go(-1);
    // @ts-ignore
    expect(history.session.cursor).toBe(0);

    history.go(3);
    // @ts-ignore
    expect(history.session.cursor).toBe(2);
  });

  it('destroy()', async () => {
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
        mockRedoFn(data);
      },
    );

    history.destroy();
    // @ts-ignore
    expect(history.records).toHaveLength(0);
  });

  it('sleep & wakeup', async () => {
    const mockRedoFn = jest.fn();
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
        mockRedoFn(data);
      },
    );

    // @ts-ignore
    history.sleep();

    await delay(1200);
    tree.data = 3;
    // no record has been pushed into records because of history is asleep.
    // @ts-ignore
    expect(history.records).toHaveLength(1);

    // @ts-ignore
    history.wakeup();
    tree.data = 4;
    // @ts-ignore
    expect(history.records).toHaveLength(2);
  });
});

describe('History - errors', () => {
  beforeAll(() => {
    globalContext.replace(Editor, null);
  });

  it('no editor', () => {
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
      },
    );

    history.back();
    history.forward();
  });

  it('no session', () => {
    const history = new History<Node>(
      () => {
        const data = tree.toObject();
        return data;
      },
      (data) => {
      },
    );

    // @ts-ignore
    history.session = undefined;
    history.back();
    history.forward();
    history.savePoint();
  });
});