/**
 * useDebouncedNodeConfig Hook Tests
 *
 * Tests debounced config management including:
 * - Debounce timer behavior
 * - Pending update flush on node switch
 * - Pending update flush on unmount
 * - Ignore flag prevents stale updates during init window
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useDebouncedNodeConfig, ConfigNode } from './useDebouncedNodeConfig';

describe('useDebouncedNodeConfig', () => {
  // ==========================================================================
  // Test Data
  // ==========================================================================

  const createNode = (overrides: Partial<ConfigNode> = {}): ConfigNode => ({
    id: 'node-1',
    type: 'action',
    name: 'Test Action',
    icon: 'Zap',
    x: 0,
    y: 0,
    status: 'valid',
    config: { field1: 'initial' },
    label: 'Test Label',
    ...overrides,
  });

  let onUpdate: ReturnType<typeof vi.fn>;
  let onClose: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    vi.useFakeTimers();
    onUpdate = vi.fn();
    onClose = vi.fn();
  });

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

  // ==========================================================================
  // Initial State Tests
  // ==========================================================================

  describe('initial state', () => {
    it('initializes actionConfig from node.config', () => {
      const node = createNode({ config: { email: 'test@example.com' } });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      expect(result.current.actionConfig).toEqual({ email: 'test@example.com' });
    });

    it('initializes with empty config when node.config is undefined', () => {
      const node = createNode({ config: undefined });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      expect(result.current.actionConfig).toEqual({});
    });

    it('initializes actionLabel from node.label', () => {
      const node = createNode({ label: 'Custom Label' });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      expect(result.current.actionLabel).toBe('Custom Label');
    });

    it('initializes triggerFilters from node.config.filters for triggers', () => {
      const filters = { logic: 'and' as const, conditions: [] };
      const node = createNode({
        type: 'trigger',
        config: { filters },
      });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      expect(result.current.triggerFilters).toEqual(filters);
    });

    it('initializes triggerFilters as null for actions', () => {
      const node = createNode({ type: 'action' });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      expect(result.current.triggerFilters).toBeNull();
    });
  });

  // ==========================================================================
  // Debounce Behavior Tests
  // ==========================================================================

  describe('debounce behavior', () => {
    it('debounces field changes with 300ms delay', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'new value');
      });

      // Should not call onUpdate immediately
      expect(onUpdate).not.toHaveBeenCalled();

      // Advance time by 299ms
      act(() => {
        vi.advanceTimersByTime(299);
      });
      expect(onUpdate).not.toHaveBeenCalled();

      // Advance to 300ms total
      act(() => {
        vi.advanceTimersByTime(1);
      });
      expect(onUpdate).toHaveBeenCalledTimes(1);
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { field1: 'new value' },
        }),
      );
    });

    it('resets debounce timer on subsequent changes', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'value1');
      });

      act(() => {
        vi.advanceTimersByTime(200);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'value2');
      });

      act(() => {
        vi.advanceTimersByTime(200);
      });

      // Still shouldn't have called - timer was reset
      expect(onUpdate).not.toHaveBeenCalled();

      act(() => {
        vi.advanceTimersByTime(100);
      });

      // Now should have called with final value
      expect(onUpdate).toHaveBeenCalledTimes(1);
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { field1: 'value2' },
        }),
      );
    });

    it('debounces label changes with same delay', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleLabelChange('New Label');
      });

      expect(onUpdate).not.toHaveBeenCalled();

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          label: 'New Label',
        }),
      );
    });
  });

  // ==========================================================================
  // Ignore Window Tests
  // ==========================================================================

  describe('ignore window', () => {
    it('ignores field changes during 100ms initialization window', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Immediately call handleFieldChange (simulating TipTap mount)
      act(() => {
        result.current.handleFieldChange('field1', 'stale value');
      });

      // Advance past debounce time
      act(() => {
        vi.advanceTimersByTime(500);
      });

      // Should not have called onUpdate because change was ignored
      expect(onUpdate).not.toHaveBeenCalled();
    });

    it('accepts field changes after 100ms initialization window', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(101);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'valid value');
      });

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { field1: 'valid value' },
        }),
      );
    });
  });

  // ==========================================================================
  // Node Switch Tests
  // ==========================================================================

  describe('node switch', () => {
    it('flushes pending update when switching to different node', () => {
      const node1 = createNode({ id: 'node-1', config: { field1: 'initial' } });
      const node2 = createNode({ id: 'node-2', config: { field2: 'other' } });

      const { result, rerender } = renderHook((props) => useDebouncedNodeConfig(props), {
        initialProps: { node: node1, onUpdate, onClose },
      });

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      // Make a change that will be pending
      act(() => {
        result.current.handleFieldChange('field1', 'pending value');
      });

      // Pending - not called yet
      expect(onUpdate).not.toHaveBeenCalled();

      // Switch to different node
      rerender({ node: node2, onUpdate, onClose });

      // Should have flushed the pending update for node-1
      expect(onUpdate).toHaveBeenCalledTimes(1);
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'node-1',
          config: { field1: 'pending value' },
        }),
      );
    });

    it('resets state when switching nodes', () => {
      const node1 = createNode({ id: 'node-1', config: { field1: 'value1' }, label: 'Label 1' });
      const node2 = createNode({ id: 'node-2', config: { field2: 'value2' }, label: 'Label 2' });

      const { result, rerender } = renderHook((props) => useDebouncedNodeConfig(props), {
        initialProps: { node: node1, onUpdate, onClose },
      });

      expect(result.current.actionConfig).toEqual({ field1: 'value1' });
      expect(result.current.actionLabel).toBe('Label 1');

      // Switch to different node
      rerender({ node: node2, onUpdate, onClose });

      expect(result.current.actionConfig).toEqual({ field2: 'value2' });
      expect(result.current.actionLabel).toBe('Label 2');
    });

    it('resets ignore window when switching nodes', () => {
      const node1 = createNode({ id: 'node-1' });
      const node2 = createNode({ id: 'node-2' });

      const { result, rerender } = renderHook((props) => useDebouncedNodeConfig(props), {
        initialProps: { node: node1, onUpdate, onClose },
      });

      // Wait for ignore window to pass on first node
      act(() => {
        vi.advanceTimersByTime(150);
      });

      // Switch to different node
      rerender({ node: node2, onUpdate, onClose });

      // Clear any pending calls from node switch
      onUpdate.mockClear();

      // Immediately try to change (should be ignored)
      act(() => {
        result.current.handleFieldChange('field1', 'should be ignored');
      });

      act(() => {
        vi.advanceTimersByTime(500);
      });

      // Should not have called because we're in new ignore window
      expect(onUpdate).not.toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // Unmount Tests
  // ==========================================================================

  describe('unmount', () => {
    it('flushes pending update on unmount', () => {
      const node = createNode();
      const { result, unmount } = renderHook(() =>
        useDebouncedNodeConfig({ node, onUpdate, onClose }),
      );

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      // Make a change that will be pending
      act(() => {
        result.current.handleFieldChange('field1', 'pending value');
      });

      expect(onUpdate).not.toHaveBeenCalled();

      // Unmount
      unmount();

      // Should have flushed the pending update
      expect(onUpdate).toHaveBeenCalledTimes(1);
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { field1: 'pending value' },
        }),
      );
    });

    it('clears debounce timer on unmount', () => {
      const node = createNode();
      const { result, unmount } = renderHook(() =>
        useDebouncedNodeConfig({ node, onUpdate, onClose }),
      );

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'value');
      });

      unmount();

      // Advance time - should not cause double call
      act(() => {
        vi.advanceTimersByTime(500);
      });

      // Should only have called once (from unmount flush)
      expect(onUpdate).toHaveBeenCalledTimes(1);
    });
  });

  // ==========================================================================
  // Close Handler Tests
  // ==========================================================================

  describe('handleClose', () => {
    it('flushes pending update before closing', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'pending value');
      });

      expect(onUpdate).not.toHaveBeenCalled();
      expect(onClose).not.toHaveBeenCalled();

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

      // Should have flushed update before calling onClose
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { field1: 'pending value' },
        }),
      );
      expect(onClose).toHaveBeenCalled();
    });

    it('calls onClose even without pending updates', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

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

      expect(onUpdate).not.toHaveBeenCalled();
      expect(onClose).toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // Trigger Filter Tests
  // ==========================================================================

  describe('handleTriggerFiltersChange', () => {
    it('updates filters immediately (no debounce)', () => {
      const node = createNode({ type: 'trigger' });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      const newFilters = { logic: 'and' as const, conditions: [] };

      act(() => {
        result.current.handleTriggerFiltersChange(newFilters);
      });

      // Should call immediately, no debounce
      expect(onUpdate).toHaveBeenCalledTimes(1);
      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: expect.objectContaining({
            filters: newFilters,
          }),
        }),
      );
    });

    it('updates local state', () => {
      const node = createNode({ type: 'trigger' });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      const newFilters = { logic: 'and' as const, conditions: [] };

      act(() => {
        result.current.handleTriggerFiltersChange(newFilters);
      });

      expect(result.current.triggerFilters).toEqual(newFilters);
    });
  });

  // ==========================================================================
  // Apply Defaults Tests
  // ==========================================================================

  describe('applyDefaults', () => {
    it('applies defaults when config is empty', () => {
      const node = createNode({ config: {} });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      act(() => {
        result.current.applyDefaults({ post_status: 'draft', post_type: 'post' });
      });

      expect(result.current.actionConfig).toEqual({ post_status: 'draft', post_type: 'post' });

      // Should trigger a debounced update
      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { post_status: 'draft', post_type: 'post' },
        }),
      );
    });

    it('does not overwrite existing config values', () => {
      const node = createNode({ config: { field1: 'value1' } });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      act(() => {
        result.current.applyDefaults({ field1: 'default', field2: 'default2' });
      });

      // Config should remain unchanged because it already has user values
      expect(result.current.actionConfig).toEqual({ field1: 'value1' });

      // Should NOT trigger a debounced update
      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).not.toHaveBeenCalled();
    });

    it('works during ignore window', () => {
      const node = createNode({ config: {} });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Call applyDefaults immediately (within 100ms ignore window)
      act(() => {
        result.current.applyDefaults({ post_status: 'draft' });
      });

      // Should still apply defaults despite ignore window
      expect(result.current.actionConfig).toEqual({ post_status: 'draft' });

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: { post_status: 'draft' },
        }),
      );
    });
  });

  // ==========================================================================
  // Edge Cases
  // ==========================================================================

  describe('edge cases', () => {
    it('auto-populates page_id when page_select changes', () => {
      const node = createNode();
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('page_select', '42');
      });

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: expect.objectContaining({
            page_select: '42',
            page_id: '42',
          }),
        }),
      );
    });

    it('preserves existing config fields when updating', () => {
      const node = createNode({ config: { existing: 'value', field1: 'old' } });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleFieldChange('field1', 'new');
      });

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          config: {
            existing: 'value',
            field1: 'new',
          },
        }),
      );
    });

    it('clears label when set to empty string', () => {
      const node = createNode({ label: 'Existing Label' });
      const { result } = renderHook(() => useDebouncedNodeConfig({ node, onUpdate, onClose }));

      // Wait for ignore window to pass
      act(() => {
        vi.advanceTimersByTime(150);
      });

      act(() => {
        result.current.handleLabelChange('');
      });

      act(() => {
        vi.advanceTimersByTime(300);
      });

      expect(onUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          label: undefined,
        }),
      );
    });
  });
});
