/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act } from 'react';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { useMessageQueue } from './useMessageQueue.js';
import { StreamingState } from '../types.js';

describe('useMessageQueue', () => {
  let mockSubmitQuery: ReturnType<typeof vi.fn>;

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

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

  const renderMessageQueueHook = async (initialProps: {
    isConfigInitialized: boolean;
    streamingState: StreamingState;
    submitQuery: (query: string) => void;
    isMcpReady: boolean;
    isCompressing?: boolean;
  }) => {
    let hookResult: ReturnType<typeof useMessageQueue>;
    function TestComponent(props: typeof initialProps) {
      hookResult = useMessageQueue(props);
      return null;
    }
    const { rerender } = await render(<TestComponent {...initialProps} />);
    return {
      result: {
        get current() {
          return hookResult;
        },
      },
      rerender: (newProps: Partial<typeof initialProps>) =>
        rerender(<TestComponent {...initialProps} {...newProps} />),
    };
  };

  it('should initialize with empty queue', async () => {
    const { result } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Idle,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    expect(result.current.messageQueue).toEqual([]);
    expect(result.current.getQueuedMessagesText()).toBe('');
  });

  it('should add messages to queue', async () => {
    const { result } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    act(() => {
      result.current.addMessage('Test message 1');
      result.current.addMessage('Test message 2');
    });

    expect(result.current.messageQueue).toEqual([
      'Test message 1',
      'Test message 2',
    ]);
  });

  it('should filter out empty messages', async () => {
    const { result } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    act(() => {
      result.current.addMessage('Valid message');
      result.current.addMessage('   '); // Only whitespace
      result.current.addMessage(''); // Empty
      result.current.addMessage('Another valid message');
    });

    expect(result.current.messageQueue).toEqual([
      'Valid message',
      'Another valid message',
    ]);
  });

  it('should clear queue', async () => {
    const { result } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    act(() => {
      result.current.addMessage('Test message');
    });

    expect(result.current.messageQueue).toEqual(['Test message']);

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

    expect(result.current.messageQueue).toEqual([]);
  });

  it('should return queued messages as text with double newlines', async () => {
    const { result } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    act(() => {
      result.current.addMessage('Message 1');
      result.current.addMessage('Message 2');
      result.current.addMessage('Message 3');
    });

    expect(result.current.getQueuedMessagesText()).toBe(
      'Message 1\n\nMessage 2\n\nMessage 3',
    );
  });

  it('should auto-submit queued messages when transitioning to Idle and MCP is ready', async () => {
    const { result, rerender } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    // Add some messages
    act(() => {
      result.current.addMessage('Message 1');
      result.current.addMessage('Message 2');
    });

    expect(result.current.messageQueue).toEqual(['Message 1', 'Message 2']);

    // Transition to Idle
    rerender({ streamingState: StreamingState.Idle });

    await waitFor(() => {
      expect(mockSubmitQuery).toHaveBeenCalledWith('Message 1\n\nMessage 2');
      expect(result.current.messageQueue).toEqual([]);
    });
  });

  it('should wait for MCP readiness before auto-submitting', async () => {
    const { result, rerender } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Idle,
      submitQuery: mockSubmitQuery,
      isMcpReady: false,
    });

    // Add some messages while Idle but MCP not ready
    act(() => {
      result.current.addMessage('Delayed message');
    });

    expect(result.current.messageQueue).toEqual(['Delayed message']);
    expect(mockSubmitQuery).not.toHaveBeenCalled();

    // Transition MCP to ready
    rerender({ isMcpReady: true });

    await waitFor(() => {
      expect(mockSubmitQuery).toHaveBeenCalledWith('Delayed message');
      expect(result.current.messageQueue).toEqual([]);
    });
  });

  it('should not auto-submit when queue is empty', async () => {
    const { rerender } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    // Transition to Idle with empty queue
    rerender({ streamingState: StreamingState.Idle });

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

  it('should not auto-submit when not transitioning to Idle', async () => {
    const { result, rerender } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Responding,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    // Add messages
    act(() => {
      result.current.addMessage('Message 1');
    });

    // Transition to WaitingForConfirmation (not Idle)
    rerender({ streamingState: StreamingState.WaitingForConfirmation });

    expect(mockSubmitQuery).not.toHaveBeenCalled();
    expect(result.current.messageQueue).toEqual(['Message 1']);
  });

  it('should handle multiple state transitions correctly', async () => {
    const { result, rerender } = await renderMessageQueueHook({
      isConfigInitialized: true,
      streamingState: StreamingState.Idle,
      submitQuery: mockSubmitQuery,
      isMcpReady: true,
    });

    // Start responding
    rerender({ streamingState: StreamingState.Responding });

    // Add messages while responding
    act(() => {
      result.current.addMessage('First batch');
    });

    // Go back to idle - should submit
    rerender({ streamingState: StreamingState.Idle });

    await waitFor(() => {
      expect(mockSubmitQuery).toHaveBeenCalledWith('First batch');
      expect(result.current.messageQueue).toEqual([]);
    });

    // Start responding again
    rerender({ streamingState: StreamingState.Responding });

    // Add more messages
    act(() => {
      result.current.addMessage('Second batch');
    });

    // Go back to idle - should submit again
    rerender({ streamingState: StreamingState.Idle });

    await waitFor(() => {
      expect(mockSubmitQuery).toHaveBeenCalledWith('Second batch');
      expect(mockSubmitQuery).toHaveBeenCalledTimes(2);
    });
  });

  describe('popAllMessages', () => {
    it('should pop all messages and return them joined with double newlines', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Responding,
        submitQuery: mockSubmitQuery,
        isMcpReady: true,
      });

      // Add multiple messages
      act(() => {
        result.current.addMessage('Message 1');
        result.current.addMessage('Message 2');
        result.current.addMessage('Message 3');
      });

      expect(result.current.messageQueue).toEqual([
        'Message 1',
        'Message 2',
        'Message 3',
      ]);

      // Pop all messages
      let poppedMessages: string | undefined;
      act(() => {
        poppedMessages = result.current.popAllMessages();
      });

      expect(poppedMessages).toBe('Message 1\n\nMessage 2\n\nMessage 3');
      expect(result.current.messageQueue).toEqual([]);
    });

    it('should return undefined when queue is empty', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Responding,
        submitQuery: mockSubmitQuery,
        isMcpReady: true,
      });

      let poppedMessages: string | undefined = 'not-undefined';
      act(() => {
        poppedMessages = result.current.popAllMessages();
      });

      expect(poppedMessages).toBeUndefined();
      expect(result.current.messageQueue).toEqual([]);
    });

    it('should handle single message correctly', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Responding,
        submitQuery: mockSubmitQuery,
        isMcpReady: false,
      });

      act(() => {
        result.current.addMessage('Single message');
      });

      let poppedMessages: string | undefined;
      act(() => {
        poppedMessages = result.current.popAllMessages();
      });

      expect(poppedMessages).toBe('Single message');
      expect(result.current.messageQueue).toEqual([]);
    });

    it('should clear the entire queue after popping', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Responding,
        submitQuery: mockSubmitQuery,
        isMcpReady: false,
      });

      act(() => {
        result.current.addMessage('Message 1');
        result.current.addMessage('Message 2');
      });

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

      // Queue should be empty
      expect(result.current.messageQueue).toEqual([]);
      expect(result.current.getQueuedMessagesText()).toBe('');

      // Popping again should return undefined
      let secondPop: string | undefined = 'not-undefined';
      act(() => {
        secondPop = result.current.popAllMessages();
      });

      expect(secondPop).toBeUndefined();
    });

    it('should work correctly with state updates', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Responding,
        submitQuery: mockSubmitQuery,
        isMcpReady: false,
      });

      // Add messages
      act(() => {
        result.current.addMessage('First');
        result.current.addMessage('Second');
      });

      // Pop all messages
      let firstPop: string | undefined;
      act(() => {
        firstPop = result.current.popAllMessages();
      });

      expect(firstPop).toBe('First\n\nSecond');

      // Add new messages after popping
      act(() => {
        result.current.addMessage('Third');
        result.current.addMessage('Fourth');
      });

      // Pop again
      let secondPop: string | undefined;
      act(() => {
        secondPop = result.current.popAllMessages();
      });

      expect(secondPop).toBe('Third\n\nFourth');
      expect(result.current.messageQueue).toEqual([]);
    });
  });

  describe('isCompressing logic', () => {
    it('should not auto-submit when isCompressing is true, even if streamingState is Idle', async () => {
      const { result } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Idle,
        submitQuery: mockSubmitQuery,
        isMcpReady: true,
        isCompressing: true,
      });

      // Add messages
      act(() => {
        result.current.addMessage('Compression message');
      });

      expect(mockSubmitQuery).not.toHaveBeenCalled();
      expect(result.current.messageQueue).toEqual(['Compression message']);
    });

    it('should auto-submit queued messages when isCompressing becomes false', async () => {
      const { result, rerender } = await renderMessageQueueHook({
        isConfigInitialized: true,
        streamingState: StreamingState.Idle,
        submitQuery: mockSubmitQuery,
        isMcpReady: true,
        isCompressing: true,
      });

      // Add messages
      act(() => {
        result.current.addMessage('Pending compression message 1');
        result.current.addMessage('Pending compression message 2');
      });

      expect(mockSubmitQuery).not.toHaveBeenCalled();

      // Transition isCompressing to false
      rerender({ isCompressing: false });

      await waitFor(() => {
        expect(mockSubmitQuery).toHaveBeenCalledWith(
          'Pending compression message 1\n\nPending compression message 2',
        );
        expect(result.current.messageQueue).toEqual([]);
      });
    });
  });
});
