import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Canvas } from '../canvas/Canvas';
import { PencilBrush } from './PencilBrush';
import { parsePath } from '../util';
import { Path } from '../shapes/Path';
import { Point } from '../Point';
import { createPointerEvent } from '../../test/utils';

describe('PencilBrush', () => {
  let canvas: Canvas;

  beforeEach(() => {
    canvas = new Canvas();
  });

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

  it('initializes constructor correctly', () => {
    expect(PencilBrush).toBeTruthy();
    const brush = new PencilBrush(canvas);
    expect(brush.canvas, 'assigns canvas').toBe(canvas);
    // @ts-expect-error -- protected
    expect(brush._points, 'points is an empty array').toEqual([]);
  });

  it('decimates points correctly', () => {
    const brush = new PencilBrush(canvas);
    const points = [
      new Point(1, 0),
      new Point(2, 0),
      new Point(3, 0),
      new Point(4, 0),
      new Point(5, 0),
    ];
    const distance = 6;
    const newPoints = brush.decimatePoints(points, distance);
    expect(newPoints[0], 'first point is always present').toBe(points[0]);
    expect(newPoints[1], 'last point is always present').toBe(
      points[points.length - 1],
    );
    expect(newPoints.length, 'All points removed except first and last').toBe(
      2,
    );
  });

  describe.each([true, false])(
    'with canvas.enableRetinaScaling = %s',
    (retinaScaling) => {
      beforeEach(() => {
        canvas.enableRetinaScaling = retinaScaling;
      });

      it('draws a point correctly', () => {
        const brush = new PencilBrush(canvas);
        const e = createPointerEvent({
          target: canvas.upperCanvasEl,
          clientX: 10,
          clientY: 10,
        });
        const pointer = canvas.getScenePoint(e);
        brush.onMouseDown(pointer, { e });
        // @ts-expect-error -- protected
        const pathData = brush.convertPointsToSVGPath(brush._points);
        expect(
          pathData,
          'path data create a small line that looks like a point',
        ).toEqual(parsePath('M 9.999 10 L 10.001 10'));
      });

      it('handles multiple coincident points', () => {
        const brush = new PencilBrush(canvas);
        const e = createPointerEvent({
          target: canvas.upperCanvasEl,
          clientX: 10,
          clientY: 10,
        });
        const pointer = canvas.getScenePoint(e);
        brush.onMouseDown(pointer, { e });
        brush.onMouseMove(pointer, { e });
        brush.onMouseMove(pointer, { e });
        brush.onMouseMove(pointer, { e });
        brush.onMouseMove(pointer, { e });
        // @ts-expect-error -- protected
        const pathData = brush.convertPointsToSVGPath(brush._points);
        expect(
          pathData,
          'path data create a small line that looks like a point',
        ).toEqual(parsePath('M 9.999 10 L 10.001 10'));
        // @ts-expect-error -- protected
        expect(brush._points.length, 'concident points are discarded').toBe(2);
      });

      it('handles multiple non-coincident points', () => {
        const brush = new PencilBrush(canvas);
        const e = createPointerEvent({ target: canvas.upperCanvasEl });
        const pointer = canvas.getScenePoint({
          ...e,
          clientX: 10,
          clientY: 10,
        });
        const pointer2 = canvas.getScenePoint({
          ...e,
          clientX: 15,
          clientY: 15,
        });
        const pointer3 = canvas.getScenePoint({
          ...e,
          clientX: 20,
          clientY: 20,
        });
        brush.onMouseDown(pointer, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        // @ts-expect-error -- protected
        const pathData = brush.convertPointsToSVGPath(brush._points);
        expect(pathData, 'path data create a complex path').toEqual(
          parsePath(
            'M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001',
          ),
        );
        // @ts-expect-error -- protected
        expect(brush._points.length, 'concident points are discarded').toBe(6);
      });

      it('handles points outside canvas', () => {
        const brush = new PencilBrush(canvas);
        const e = createPointerEvent({ target: canvas.upperCanvasEl });
        const pointer = canvas.getScenePoint({
          ...e,
          clientX: 10,
          clientY: 10,
        });
        const pointer2 = canvas.getScenePoint({
          ...e,
          clientX: 15,
          clientY: 100,
        });
        const pointer3 = canvas.getScenePoint({
          ...e,
          clientX: 20,
          clientY: 160,
        });
        const pointer4 = canvas.getScenePoint({
          ...e,
          clientX: 320,
          clientY: 100,
        });
        const pointer5 = canvas.getScenePoint({
          ...e,
          clientX: 100,
          clientY: 100,
        });
        brush.onMouseDown(pointer, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        brush.onMouseMove(pointer4, { e });
        brush.onMouseMove(pointer5, { e });
        // @ts-expect-error -- protected
        const pathData = brush.convertPointsToSVGPath(brush._points);
        expect(
          pathData,
          'path data create a path that goes beyond canvas',
        ).toEqual(
          parsePath(
            'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 17.5 130 Q 20 160 170 130 Q 320 100 210 100 L 99.999 100',
          ),
        );
        // @ts-expect-error -- protected
        expect(brush._points.length, 'all points are available').toBe(6);
      });

      it('limits points to canvas size when limitedToCanvasSize is true', () => {
        const brush = new PencilBrush(canvas);
        brush.limitedToCanvasSize = true;
        const e = createPointerEvent({ target: canvas.upperCanvasEl });
        const pointer = canvas.getScenePoint({
          ...e,
          clientX: 10,
          clientY: 10,
        });
        const pointer2 = canvas.getScenePoint({
          ...e,
          clientX: 15,
          clientY: 100,
        });
        const pointer3 = canvas.getScenePoint({
          ...e,
          clientX: 20,
          clientY: 160,
        });
        const pointer4 = canvas.getScenePoint({
          ...e,
          clientX: 320,
          clientY: 100,
        });
        const pointer5 = canvas.getScenePoint({
          ...e,
          clientX: 100,
          clientY: 100,
        });
        brush.onMouseDown(pointer, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        brush.onMouseMove(pointer4, { e });
        brush.onMouseMove(pointer5, { e });
        // @ts-expect-error -- protected
        const pathData = brush.convertPointsToSVGPath(brush._points);
        expect(
          pathData,
          'path data create a path that does not go beyond canvas',
        ).toEqual(
          parsePath(
            'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 57.5 100 L 100.001 100',
          ),
        );
        // @ts-expect-error -- protected
        expect(brush._points.length, '2 points have been discarded').toBe(4);
      });

      it('fires events and creates path on mouse up', () => {
        let fireBeforePathCreatedEvent = false;
        let firePathCreatedEvent = false;
        let added = null;

        canvas.on('before:path:created', () => {
          fireBeforePathCreatedEvent = true;
        });

        canvas.on('path:created', (opt) => {
          firePathCreatedEvent = true;
          added = opt.path;
        });

        const brush = new PencilBrush(canvas);
        const e = createPointerEvent({ target: canvas.upperCanvasEl });
        const pointer = canvas.getScenePoint({
          ...e,
          clientX: 10,
          clientY: 10,
        });
        const pointer2 = canvas.getScenePoint({
          ...e,
          clientX: 15,
          clientY: 15,
        });
        const pointer3 = canvas.getScenePoint({
          ...e,
          clientX: 20,
          clientY: 20,
        });

        brush.onMouseDown(pointer, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        brush.onMouseMove(pointer2, { e });
        brush.onMouseMove(pointer3, { e });
        brush.onMouseUp({ e });

        expect(
          fireBeforePathCreatedEvent,
          'before:path:created event is fired',
        ).toBe(true);
        expect(firePathCreatedEvent, 'path:created event is fired').toBe(true);
        expect(added, 'a path is added').toBeInstanceOf(Path);
        expect(added!.path).toMatchSnapshot();
        expect(added!.path.length, 'path has 6 steps').toBe(4);
      });
    },
  );
});
