import {
  describe,
  test,
  expect,
  beforeAll,
  afterAll,
  afterEach,
  beforeEach,
} from 'vitest';
import { Canvas } from '../../canvas/Canvas';
import { IText } from './IText';
import { Group } from '../Group';
import { config } from '../../config';
import type {
  ObjectPointerEvents,
  TPointerEventInfo,
} from '../../EventTypeDefs';
import { Point } from '../../Point';
import { createPointerEvent } from '../../../test/utils';

describe('iText click interaction', () => {
  let canvas: Canvas;

  beforeAll(() => {
    canvas = new Canvas(undefined, {
      enableRetinaScaling: false,
    });
  });

  afterAll(() => canvas.dispose());

  afterEach(() => {
    canvas.clear();
    canvas.cancelRequestedRender();
  });

  test('doubleClickHandler', async () => {
    const iText = new IText('test need some word\nsecond line');
    iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');

    iText.canvas = canvas;

    let eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 10,
    });

    iText.enterEditing();

    iText.doubleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'dblClick selection start is').toBe(0);
    expect(iText.selectionEnd, 'dblClick selection end is').toBe(4);

    eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 60,
    });

    iText.doubleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'second dblClick selection start is').toBe(20);
    expect(iText.selectionEnd, 'second dblClick selection end is').toBe(26);
  });

  test('doubleClickHandler no editing', () => {
    const iText = new IText('test need some word\nsecond line');

    iText.canvas = canvas;

    const eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 10,
    });

    iText.doubleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'dblClick selection start is').toBe(0);
    expect(iText.selectionEnd, 'dblClick selection end is').toBe(0);
  });

  test('tripleClickHandler', async () => {
    const iText = new IText('test need some word\nsecond line');
    iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');

    iText.canvas = canvas;

    let eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 10,
    });

    iText.enterEditing();

    iText.tripleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'tripleClick selection start is').toBe(0);
    expect(iText.selectionEnd, 'tripleClick selection end is').toBe(19);

    eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 60,
    });

    iText.tripleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'second tripleClick selection start is').toBe(
      20,
    );
    expect(iText.selectionEnd, 'second tripleClick selection end is').toBe(31);

    iText.exitEditing();
  });

  test('tripleClickHandler without editing', () => {
    const iText = new IText('test need some word\nsecond line');

    iText.canvas = canvas;

    const eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 40,
      clientY: 10,
    });

    iText.tripleClickHandler({
      e: eventData,
    } as unknown as TPointerEventInfo);

    expect(iText.selectionStart, 'tripleClick selection start is').toBe(0);
    expect(iText.selectionEnd, 'tripleClick selection end is').toBe(0);
  });

  test('getSelectionStartFromPointer with scale', () => {
    const eventData = createPointerEvent({
      target: canvas.upperCanvasEl,
      clientX: 70,
      clientY: 10,
    });

    const iText = new IText('test need some word\nsecond line', {
      scaleX: 3,
      scaleY: 2,
      canvas,
    });
    iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
    expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(2);
    expect(
      iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
      'index',
    ).toBe(2);

    iText.set({ scaleX: 0.5, scaleY: 0.25 });
    iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');

    expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(9);
    expect(
      iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
      'index',
    ).toBe(29);

    iText.set({ scaleX: 1, scaleY: 1 });
    iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');

    expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(5);
    expect(
      iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
      'index',
    ).toBe(5);
  });

  test('mouse down aborts cursor animation', () => {
    const iText = new IText('test need some word\nsecond line', {
      canvas,
    });

    expect(iText._animateCursor, 'method is defined').toBeTypeOf('function');

    let animate = 0;
    let aborted = 0;

    // @ts-expect-error -- overridden for test simplicity
    iText._animateCursor = () => animate++;
    iText.abortCursorAnimation = () => aborted++;

    canvas.setActiveObject(iText);
    iText.enterEditing();
    iText._mouseDownHandler({
      e: { target: canvas.upperCanvasEl },
    } as unknown as ObjectPointerEvents['mousedown']);

    expect(animate, 'called from enterEditing').toBe(1);
    expect(aborted, 'called from render').toBe(1);
  });

  test('_mouseUpHandler on a selected object enter edit', () => {
    const iText = new IText('test');

    iText.initDelayedCursor = function () {};
    iText.renderCursorOrSelection = function () {};

    expect(iText.isEditing, 'iText not editing').toBe(false);

    iText.canvas = canvas;
    canvas._activeObject = undefined;
    // @ts-expect-error -- protected member
    iText.selected = true;

    iText.mouseUpHandler({
      e: {},
    } as unknown as ObjectPointerEvents['mouseup']);

    expect(iText.isEditing, 'iText entered editing').toBe(true);

    iText.exitEditing();
  });

  test('_mouseUpHandler on a selected object does enter edit if there is an activeObject', () => {
    const iText = new IText('test');

    iText.initDelayedCursor = function () {};
    iText.renderCursorOrSelection = function () {};

    expect(iText.isEditing, 'iText not editing').toBe(false);

    iText.canvas = canvas;
    canvas._activeObject = new IText('test2');
    // @ts-expect-error -- protected member
    iText.selected = true;

    iText.mouseUpHandler({
      e: {},
    } as unknown as ObjectPointerEvents['mouseup']);

    expect(iText.isEditing, 'iText should not enter editing').toBe(false);

    iText.exitEditing();
  });

  test('_mouseUpHandler on a selected text in a group does NOT enter editing', () => {
    const iText = new IText('test');

    iText.initDelayedCursor = function () {};
    iText.renderCursorOrSelection = function () {};

    expect(iText.isEditing, 'iText not editing').toBe(false);

    const group = new Group([iText], { subTargetCheck: false });

    canvas.add(group);
    // @ts-expect-error -- protected member
    iText.selected = true;
    const evt = createPointerEvent({
      clientX: 1,
      clientY: 1,
      target: canvas.upperCanvasEl,
    });
    canvas._cacheTransformEventData(evt);
    canvas.__onMouseUp(evt);
    // @ts-expect-error -- protected member
    expect(canvas._targetInfo.target, 'group should be found as target').toBe(
      group,
    );
    expect(iText.isEditing, 'iText should not enter editing').toBe(false);

    iText.exitEditing();
    canvas._resetTransformEventData();
  });

  test('_mouseUpHandler on a text in a group', () => {
    const iText = new IText('test');

    iText.initDelayedCursor = function () {};
    iText.renderCursorOrSelection = function () {};

    expect(iText.isEditing, 'iText not editing').toBe(false);

    const group = new Group([iText], {
      subTargetCheck: true,
      interactive: true,
    });

    canvas.add(group);
    // @ts-expect-error -- protected member
    iText.selected = true;

    canvas._onMouseUp(
      createPointerEvent({
        clientX: 1,
        clientY: 1,
        target: canvas.upperCanvasEl,
      }),
    );

    expect(iText.isEditing, 'iText should enter editing').toBe(true);

    iText.exitEditing();
    group.interactive = false;
    // @ts-expect-error -- protected member
    iText.selected = true;

    canvas._onMouseUp(
      createPointerEvent({
        clientX: 1,
        clientY: 1,
        target: canvas.upperCanvasEl,
      }),
    );

    expect(iText.isEditing, 'iText should not enter editing').toBe(false);
  });

  test('_mouseUpHandler on a corner of selected text DOES NOT enter edit', () => {
    const iText = new IText('test');

    iText.initDelayedCursor = function () {};
    iText.renderCursorOrSelection = function () {};

    expect(iText.isEditing, 'iText not editing').toBe(false);

    iText.canvas = canvas;
    // @ts-expect-error -- protected member
    iText.selected = true;
    iText.__corner = 'mt';
    iText.setCoords();

    iText.mouseUpHandler({
      e: {},
    } as unknown as ObjectPointerEvents['mouseup']);

    expect(iText.isEditing, 'iText should not enter editing').toBe(false);

    iText.exitEditing();
    canvas.renderAll();
  });

  [true, false].forEach((enableRetinaScaling) => {
    describe(`enableRetinaScaling = ${enableRetinaScaling}`, () => {
      let testCanvas: Canvas;
      let eventData: TPointerEvent;
      let iText: IText;
      let count: number;
      let countCanvas: number;

      beforeAll(() => {
        config.configure({ devicePixelRatio: 2 });
      });

      afterAll(() => {
        config.restoreDefaults();
      });

      beforeEach(() => {
        testCanvas = new Canvas(undefined, {
          enableRetinaScaling,
        });

        eventData = createPointerEvent({
          target: testCanvas.upperCanvasEl,
          ...(enableRetinaScaling
            ? { clientX: 60, clientY: 30 }
            : { clientX: 30, clientY: 15 }),
        });

        count = 0;
        countCanvas = 0;

        iText = new IText('test test');
        iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
        testCanvas.add(iText);

        testCanvas.on('text:selection:changed', () => {
          countCanvas++;
        });

        iText.on('selection:changed', () => {
          count++;
        });
      });

      afterEach(() => testCanvas.dispose());

      test(`click on editing itext make selection:changed fire`, async () => {
        expect(
          testCanvas.getActiveObject(),
          'no active object exist',
        ).toBeUndefined();
        expect(count, 'no selection:changed fired yet').toBe(0);
        expect(countCanvas, 'no text:selection:changed fired yet').toBe(0);

        testCanvas._onMouseDown(eventData);
        testCanvas._onMouseUp(eventData);

        expect(testCanvas.getActiveObject(), 'Itext got selected').toBe(iText);
        expect(iText.isEditing, 'Itext is not editing yet').toBe(false);
        expect(count, 'no selection:changed fired yet').toBe(0);
        expect(countCanvas, 'no text:selection:changed fired yet').toBe(0);
        expect(
          iText.selectionStart,
          'Itext did not set the selectionStart',
        ).toBe(0);
        expect(iText.selectionEnd, 'Itext did not set the selectionend').toBe(
          0,
        );

        // make a little delay or it will act as double click and select everything
        await new Promise((resolve) => setTimeout(resolve, 500));

        testCanvas._onMouseDown(eventData);
        testCanvas._onMouseUp(eventData);

        expect(iText.isEditing, 'Itext entered editing').toBe(true);
        expect(iText.selectionStart, 'Itext set the selectionStart').toBe(2);
        expect(iText.selectionEnd, 'Itext set the selectionend').toBe(2);
        expect(count, 'no selection:changed fired yet').toBe(1);
        expect(countCanvas, 'no text:selection:changed fired yet').toBe(1);
      });
    });
  });
});
