import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useUndoRedoWithState } from './useUndoRedo';
import { Node, Edge } from '@xyflow/react';

// Helper to create mock nodes
const createNode = (id: string, type = 'action'): Node => ({
  id,
  type,
  position: { x: 0, y: 0 },
  data: { name: `Node ${id}` },
});

describe('useUndoRedoWithState', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  // Helper to create a hook with state
  const createHookWithState = (initialNodes: Node[] = [], initialEdges: Edge[] = []) => {
    const setNodes = vi.fn();
    const setEdges = vi.fn();
    return { setNodes, setEdges, initialNodes, initialEdges };
  };

  describe('initial state', () => {
    it('should start with empty history', () => {
      const { setNodes, setEdges } = createHookWithState();
      const { result } = renderHook(() => useUndoRedoWithState([], [], setNodes, setEdges));

      expect(result.current.canUndo).toBe(false);
      expect(result.current.canRedo).toBe(false);
      expect(result.current.undoCount).toBe(0);
      expect(result.current.redoCount).toBe(0);
    });
  });

  describe('takeSnapshot', () => {
    it('should add snapshot to history', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      expect(result.current.canUndo).toBe(true);
      expect(result.current.undoCount).toBe(1);
    });

    it('should debounce rapid snapshots', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      // Take multiple snapshots rapidly
      act(() => {
        result.current.takeSnapshot(nodes, []);
        result.current.takeSnapshot(nodes, []);
        result.current.takeSnapshot(nodes, []);
      });

      // Should only have 1 snapshot due to debouncing
      expect(result.current.undoCount).toBe(1);
    });

    it('should accept snapshots after debounce interval', () => {
      const nodes = [createNode('node-1')];
      const nodes2 = [createNode('node-2')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      // Advance past debounce interval (100ms)
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.takeSnapshot(nodes2, []);
      });

      expect(result.current.undoCount).toBe(2);
    });
  });

  describe('undo', () => {
    it('should do nothing when no history', () => {
      const { setNodes, setEdges } = createHookWithState();
      const { result } = renderHook(() => useUndoRedoWithState([], [], setNodes, setEdges));

      act(() => {
        result.current.undo();
      });

      expect(setNodes).not.toHaveBeenCalled();
      expect(setEdges).not.toHaveBeenCalled();
    });

    it('should restore previous state on undo', () => {
      const initialNodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(initialNodes);
      const { result, rerender } = renderHook(
        ({ nodes, edges }) => useUndoRedoWithState(nodes, edges, setNodes, setEdges),
        { initialProps: { nodes: initialNodes, edges: [] as Edge[] } },
      );

      // Take snapshot of initial state
      act(() => {
        result.current.takeSnapshot(initialNodes, []);
      });

      // Simulate state change (add a node)
      const newNodes = [...initialNodes, createNode('node-2')];
      rerender({ nodes: newNodes, edges: [] });

      // Undo should restore to snapshot
      act(() => {
        result.current.undo();
      });

      // setNodes should have been called with the original nodes
      expect(setNodes).toHaveBeenCalled();
    });

    it('should enable redo after undo', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      expect(result.current.canRedo).toBe(false);

      act(() => {
        result.current.undo();
      });

      expect(result.current.canRedo).toBe(true);
      expect(result.current.redoCount).toBe(1);
    });

    it('should support multiple undos', () => {
      const nodes1 = [createNode('node-1')];
      const nodes2 = [createNode('node-2')];
      const nodes3 = [createNode('node-3')];
      const { setNodes, setEdges } = createHookWithState(nodes1);
      const { result } = renderHook(() => useUndoRedoWithState(nodes1, [], setNodes, setEdges));

      // Create multiple snapshots
      act(() => {
        result.current.takeSnapshot(nodes1, []);
        vi.advanceTimersByTime(150);
        result.current.takeSnapshot(nodes2, []);
        vi.advanceTimersByTime(150);
        result.current.takeSnapshot(nodes3, []);
      });

      expect(result.current.undoCount).toBe(3);

      // First undo
      act(() => {
        result.current.undo();
      });
      expect(result.current.undoCount).toBe(2);

      // Second undo
      act(() => {
        result.current.undo();
      });
      expect(result.current.undoCount).toBe(1);

      // Third undo
      act(() => {
        result.current.undo();
      });
      expect(result.current.undoCount).toBe(0);
    });
  });

  describe('redo', () => {
    it('should do nothing when no redo history', () => {
      const { setNodes, setEdges } = createHookWithState();
      const { result } = renderHook(() => useUndoRedoWithState([], [], setNodes, setEdges));

      act(() => {
        result.current.redo();
      });

      expect(setNodes).not.toHaveBeenCalled();
      expect(setEdges).not.toHaveBeenCalled();
    });

    it('should redo after undo', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      act(() => {
        result.current.undo();
      });

      expect(result.current.canRedo).toBe(true);
      setNodes.mockClear();

      act(() => {
        result.current.redo();
      });

      expect(setNodes).toHaveBeenCalled();
      expect(result.current.canRedo).toBe(false);
    });
  });

  describe('clearHistory', () => {
    it('should clear all history', () => {
      const nodes = [createNode('node-1')];
      const nodes2 = [createNode('node-2')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
        vi.advanceTimersByTime(150);
        result.current.takeSnapshot(nodes2, []);
      });

      expect(result.current.undoCount).toBe(2);

      act(() => {
        result.current.clearHistory();
      });

      expect(result.current.canUndo).toBe(false);
      expect(result.current.canRedo).toBe(false);
      expect(result.current.undoCount).toBe(0);
      expect(result.current.redoCount).toBe(0);
    });

    it('should reset debounce timer', () => {
      const nodes = [createNode('node-1')];
      const nodes2 = [createNode('node-2')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      act(() => {
        result.current.clearHistory();
      });

      // Should be able to take snapshot immediately after clear
      act(() => {
        result.current.takeSnapshot(nodes2, []);
      });

      expect(result.current.undoCount).toBe(1);
    });
  });

  describe('history limit', () => {
    it('should limit history to MAX_HISTORY_LENGTH (50)', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      // Add more than 50 snapshots
      for (let i = 0; i < 60; i++) {
        act(() => {
          vi.advanceTimersByTime(150);
          result.current.takeSnapshot([createNode(`node-${i}`)], []);
        });
      }

      // Should be capped at 50
      expect(result.current.undoCount).toBe(50);
    });
  });

  describe('canUndo and canRedo', () => {
    it('should accurately reflect undo availability', () => {
      const nodes = [createNode('node-1')];
      const { setNodes, setEdges } = createHookWithState(nodes);
      const { result } = renderHook(() => useUndoRedoWithState(nodes, [], setNodes, setEdges));

      expect(result.current.canUndo).toBe(false);

      act(() => {
        result.current.takeSnapshot(nodes, []);
      });

      expect(result.current.canUndo).toBe(true);

      act(() => {
        result.current.undo();
      });

      expect(result.current.canUndo).toBe(false);
    });
  });
});
