import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCanvasState } from '../useCanvasState';
import type { RectObject, CircleObject } from '../../types/canvas';

function makeRect(id: string, overrides: Partial<RectObject> = {}): RectObject {
  return {
    type: 'rect',
    id,
    name: `Rect ${id}`,
    x: 0,
    y: 0,
    width: 100,
    height: 50,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    fill: '#fff',
    stroke: '#000',
    strokeWidth: 1,
    ...overrides,
  };
}

function makeCircle(id: string): CircleObject {
  return {
    type: 'circle',
    id,
    name: `Circle ${id}`,
    x: 50,
    y: 50,
    radius: 25,
    rotation: 0,
    visible: true,
    locked: false,
    opacity: 1,
    fill: '#fff',
    stroke: '#000',
    strokeWidth: 1,
  };
}

describe('useCanvasState', () => {
  it('starts with empty objects and no artboard', () => {
    const { result } = renderHook(() => useCanvasState());
    expect(result.current.state.objects).toEqual([]);
    expect(result.current.state.selectedIds).toEqual([]);
    expect(result.current.state.artboard).toBeNull();
  });

  describe('addObject', () => {
    it('adds an object to the canvas', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      expect(result.current.state.objects).toHaveLength(1);
      expect(result.current.state.objects[0].id).toBe('r1');
    });
  });

  describe('removeObject', () => {
    it('removes an object by id', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.addObject(makeRect('r2')));
      act(() => result.current.removeObject('r1'));
      expect(result.current.state.objects).toHaveLength(1);
      expect(result.current.state.objects[0].id).toBe('r2');
    });

    it('also removes the id from selectedIds', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.selectObjects(['r1']));
      act(() => result.current.removeObject('r1'));
      expect(result.current.state.selectedIds).toEqual([]);
    });
  });

  describe('updateObject', () => {
    it('merges changes into an existing object', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.updateObject('r1', { x: 42, y: 99 }));
      expect(result.current.state.objects[0].x).toBe(42);
      expect(result.current.state.objects[0].y).toBe(99);
    });
  });

  describe('selectObjects / deselectAll', () => {
    it('sets selected IDs', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.addObject(makeRect('r2')));
      act(() => result.current.selectObjects(['r1', 'r2']));
      expect(result.current.state.selectedIds).toEqual(['r1', 'r2']);
    });

    it('deselects all', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.selectObjects(['r1']));
      act(() => result.current.deselectAll());
      expect(result.current.state.selectedIds).toEqual([]);
    });
  });

  describe('reorderObject', () => {
    it('moves an object to a new z-index', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('a')));
      act(() => result.current.addObject(makeRect('b')));
      act(() => result.current.addObject(makeCircle('c')));
      // Move 'a' to the end (topmost)
      act(() => result.current.reorderObject('a', 2));
      expect(result.current.state.objects.map((o) => o.id)).toEqual([
        'b', 'c', 'a',
      ]);
    });

    it('clamps toIndex to valid range', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('a')));
      act(() => result.current.addObject(makeRect('b')));
      act(() => result.current.reorderObject('a', 999));
      expect(result.current.state.objects.map((o) => o.id)).toEqual(['b', 'a']);
    });

    it('no-ops for unknown id', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('a')));
      act(() => result.current.reorderObject('nope', 0));
      expect(result.current.state.objects).toHaveLength(1);
    });
  });

  describe('toggleVisibility / toggleLock', () => {
    it('toggles visibility', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      expect(result.current.state.objects[0].visible).toBe(true);
      act(() => result.current.toggleVisibility('r1'));
      expect(result.current.state.objects[0].visible).toBe(false);
      act(() => result.current.toggleVisibility('r1'));
      expect(result.current.state.objects[0].visible).toBe(true);
    });

    it('toggles lock', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      expect(result.current.state.objects[0].locked).toBe(false);
      act(() => result.current.toggleLock('r1'));
      expect(result.current.state.objects[0].locked).toBe(true);
    });
  });

  describe('setArtboard', () => {
    it('sets the artboard config', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() =>
        result.current.setArtboard({
          shape: 'rect',
          width: 4,
          height: 6,
          unit: 'inches',
        }),
      );
      expect(result.current.state.artboard).toEqual({
        shape: 'rect',
        width: 4,
        height: 6,
        unit: 'inches',
      });
    });
  });

  describe('undo / redo', () => {
    it('undoes the last mutation', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      expect(result.current.state.objects).toHaveLength(1);
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(0);
    });

    it('redoes after undo', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(0);
      act(() => result.current.redo());
      expect(result.current.state.objects).toHaveLength(1);
    });

    it('undo is no-op when stack is empty', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.undo()); // should not throw
      expect(result.current.state.objects).toHaveLength(0);
    });

    it('redo is no-op when stack is empty', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.redo()); // should not throw
      expect(result.current.state.objects).toHaveLength(0);
    });

    it('new mutation clears the redo stack', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.undo());
      // Now add a different object — redo should be gone
      act(() => result.current.addObject(makeCircle('c1')));
      act(() => result.current.redo()); // no-op
      expect(result.current.state.objects).toHaveLength(1);
      expect(result.current.state.objects[0].id).toBe('c1');
    });

    it('undoes through multiple mutations', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.addObject(makeRect('r2')));
      act(() => result.current.addObject(makeRect('r3')));
      expect(result.current.state.objects).toHaveLength(3);
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(2);
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(1);
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(0);
    });

    it('select/deselect do not push to undo stack', () => {
      const { result } = renderHook(() => useCanvasState());
      act(() => result.current.addObject(makeRect('r1')));
      act(() => result.current.selectObjects(['r1']));
      act(() => result.current.deselectAll());
      // undo should revert addObject, not select/deselect
      act(() => result.current.undo());
      expect(result.current.state.objects).toHaveLength(0);
    });
  });
});
