/**
 * Tests for VoiceContext race condition fix
 *
 * These tests verify that the AbortController race condition is properly handled:
 * - When a new request supersedes an old one, the old request is properly aborted
 * - The old request's response is discarded if it arrives after being superseded
 * - Session stop properly cancels in-flight requests
 */

// Mock dependencies before imports
jest.mock('../../services/api', () => ({
  api: {
    getVoiceApiType: jest.fn(),
    getDeploymentId: jest.fn(),
    setVoiceApiType: jest.fn(),
  },
}));

jest.mock('expo-speech', () => ({
  speak: jest.fn(),
  stop: jest.fn(),
  isSpeakingAsync: jest.fn(),
}));

jest.mock('../VoiceTranscriptContext', () => ({
  useVoiceTranscript: () => ({
    addTranscriptEntry: jest.fn(),
  }),
  VoiceTranscriptProvider: ({ children }: any) => children,
}));

// Mock fetch
global.fetch = jest.fn();

import { renderHook, act, waitFor } from '@testing-library/react-native';
import { VoiceProvider, useVoice } from '../VoiceContext';
import { api } from '../../services/api';
import * as Speech from 'expo-speech';
import React from 'react';

describe('VoiceContext - AbortController Race Condition', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (api.getVoiceApiType as jest.Mock).mockResolvedValue('ask_wellnuo_ai');
    (api.getDeploymentId as jest.Mock).mockResolvedValue('21');
    (Speech.speak as jest.Mock).mockImplementation((text, options) => {
      setTimeout(() => options?.onDone?.(), 0);
    });
  });

  const mockApiResponse = (token: string, responseText: string, delay = 0) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          json: async () => ({
            ok: true,
            response: { body: responseText },
          }),
        });
      }, delay);
    });
  };

  const mockTokenResponse = () => ({
    json: async () => ({
      status: '200 OK',
      access_token: 'test-token',
    }),
  });

  it('should abort old request when new request comes in', async () => {
    const abortedRequests: AbortSignal[] = [];

    (global.fetch as jest.Mock).mockImplementation((url, options) => {
      const signal = options?.signal;
      if (signal) {
        abortedRequests.push(signal);
      }

      // First call - token request
      if (url.includes('function=credentials')) {
        return Promise.resolve(mockTokenResponse());
      }

      // Subsequent calls - API requests
      return mockApiResponse('test-token', 'Response', 100);
    });

    const { result } = renderHook(() => useVoice(), {
      wrapper: ({ children }) => <VoiceProvider>{children}</VoiceProvider>,
    });

    await act(async () => {
      result.current.startSession();
    });

    // Send first request
    act(() => {
      result.current.sendTranscript('First message');
    });

    // Wait a bit, then send second request (should abort first)
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 10));
      result.current.sendTranscript('Second message');
    });

    // Wait for requests to complete
    await waitFor(() => {
      expect(abortedRequests.length).toBeGreaterThan(0);
    }, { timeout: 3000 });

    // Verify that at least one request signal was aborted
    const hasAbortedSignal = abortedRequests.some(signal => signal.aborted);
    expect(hasAbortedSignal).toBe(true);
  });

  it('should discard response from superseded request', async () => {
    let requestCount = 0;
    const responses = ['First response', 'Second response'];

    (global.fetch as jest.Mock).mockImplementation((url, options) => {
      // Token request
      if (url.includes('function=credentials')) {
        return Promise.resolve(mockTokenResponse());
      }

      // API requests - first one is slower
      const currentRequest = requestCount++;
      const delay = currentRequest === 0 ? 200 : 50; // First request is slower
      return mockApiResponse('test-token', responses[currentRequest], delay);
    });

    const { result } = renderHook(() => useVoice(), {
      wrapper: ({ children }) => <VoiceProvider>{children}</VoiceProvider>,
    });

    await act(async () => {
      result.current.startSession();
    });

    // Send first request (will be slow)
    const firstPromise = act(async () => {
      return await result.current.sendTranscript('First message');
    });

    // Immediately send second request (will be fast and supersede first)
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 10));
      await result.current.sendTranscript('Second message');
    });

    await firstPromise;

    // Wait for all promises to settle
    await waitFor(() => {
      // The lastResponse should be from the second request only
      // because the first request's response should be discarded
      expect(result.current.lastResponse).toBe('Second response');
    }, { timeout: 3000 });
  });

  it('should abort request when session is stopped', async () => {
    let abortedSignal: AbortSignal | null = null;

    (global.fetch as jest.Mock).mockImplementation((url, options) => {
      const signal = options?.signal;
      if (signal && !url.includes('function=credentials')) {
        abortedSignal = signal;
      }

      // Token request
      if (url.includes('function=credentials')) {
        return Promise.resolve(mockTokenResponse());
      }

      // API request - slow
      return mockApiResponse('test-token', 'Response', 200);
    });

    const { result } = renderHook(() => useVoice(), {
      wrapper: ({ children }) => <VoiceProvider>{children}</VoiceProvider>,
    });

    await act(async () => {
      result.current.startSession();
    });

    // Send request
    act(() => {
      result.current.sendTranscript('Test message');
    });

    // Stop session while request is in flight
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 50));
      result.current.stopSession();
    });

    // Wait a bit for abort to process
    await waitFor(() => {
      expect(abortedSignal?.aborted).toBe(true);
    }, { timeout: 3000 });

    // Session should be idle
    expect(result.current.status).toBe('idle');
  });

  it('should not speak response if session stopped during API call', async () => {
    (global.fetch as jest.Mock).mockImplementation((url, options) => {
      // Token request
      if (url.includes('function=credentials')) {
        return Promise.resolve(mockTokenResponse());
      }

      // API request - slow
      return mockApiResponse('test-token', 'Response text', 100);
    });

    const { result } = renderHook(() => useVoice(), {
      wrapper: ({ children }) => <VoiceProvider>{children}</VoiceProvider>,
    });

    await act(async () => {
      result.current.startSession();
    });

    // Send request
    const transcriptPromise = act(async () => {
      return await result.current.sendTranscript('Test message');
    });

    // Stop session while API call is in flight
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 30));
      result.current.stopSession();
    });

    await transcriptPromise;

    // Wait for any pending operations
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 200));
    });

    // Speech.speak should not have been called with the response
    // (might be called with error message, but not with "Response text")
    const speakCalls = (Speech.speak as jest.Mock).mock.calls;
    const hasResponseText = speakCalls.some(call => call[0] === 'Response text');
    expect(hasResponseText).toBe(false);
  });

  it('should handle retry with proper abort controller check', async () => {
    let requestCount = 0;

    (global.fetch as jest.Mock).mockImplementation((url, options) => {
      // Token request
      if (url.includes('function=credentials')) {
        return Promise.resolve(mockTokenResponse());
      }

      // First API request - return 401 to trigger retry
      if (requestCount === 0) {
        requestCount++;
        return Promise.resolve({
          json: async () => ({
            status: '401 Unauthorized',
            ok: false,
          }),
        });
      }

      // Retry request - slow
      return mockApiResponse('test-token', 'Retry response', 100);
    });

    const { result } = renderHook(() => useVoice(), {
      wrapper: ({ children }) => <VoiceProvider>{children}</VoiceProvider>,
    });

    await act(async () => {
      result.current.startSession();
    });

    // Send first request (will trigger retry)
    const firstPromise = act(async () => {
      return await result.current.sendTranscript('First message');
    });

    // Send second request during retry (should supersede first)
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 50));
      await result.current.sendTranscript('Second message');
    });

    await firstPromise;

    // Wait for operations to complete
    await waitFor(() => {
      // Should not have "Retry response" because first request was superseded
      expect(result.current.lastResponse).not.toBe('Retry response');
    }, { timeout: 3000 });
  });
});
