import { expectSaga, testSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';
import { throwError, dynamic } from 'redux-saga-test-plan/providers';
import { call, put } from 'redux-saga/effects';
import { pollAssetStatus } from '../assetPolling';
import { getAssetStatus } from '../../services/api';

// Dynamic provider for delay calls - matches call effects with a single number argument
// This is needed because function references don't match between modules
// The effect structure: { '@@redux-saga/IO': true, CALL: { context: null, fn: delay, args: [ms] } }
const delayProvider = (effect, next) => {
  // Check if this is a call effect
  if (effect && effect.CALL) {
    const args = effect.CALL.args;
    // Match any call with a single number argument (delay calls)
    // delay(ms) calls have exactly one numeric argument
    if (Array.isArray(args) && args.length === 1 && typeof args[0] === 'number' && args[0] >= 0) {
      // Return resolved promise immediately to skip actual delay
      return Promise.resolve();
    }
  }
  // Not a delay call, pass to next provider/matcher
  return next();
};

// Increase timeout for tests that involve delays
jest.setTimeout(20000);

describe('assetPolling saga', () => {
  describe('pollAssetStatus', () => {
    const mockAssetId = 'asset-123';
    const mockAssetType = 'image';
    const mockAsset = { _id: mockAssetId, type: 'IMAGE', url: 'https://example.com/image.jpg' };
    
    // Mock Date.now() to return predictable values for duration calculation
    const mockStartTime = 1000;
    let originalDateNow;
    
    beforeEach(() => {
      originalDateNow = Date.now;
      // Mock Date.now() to always return the same value so duration = 0
      Date.now = jest.fn(() => mockStartTime);
    });
    
    afterEach(() => {
      Date.now = originalDateNow;
    });

    const createMockConfig = (overrides = {}) => ({
      type: mockAssetType,
      assetId: mockAssetId,
      onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
      onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
      onTimeout: (data) => ({ type: 'ASSET_TIMEOUT', payload: data }),
      maxDuration: 50000, // Longer duration to avoid timeout issues
      initialDelay: 0, // No delay for faster tests
      pollInterval: 0, // No interval for faster tests
      ...overrides,
    });

    it('should complete successfully when asset status is completed on first poll', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'completed',
                asset: mockAsset,
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_COMPLETED',
          payload: {
            assetId: mockAssetId,
            asset: mockAsset,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle failed status immediately', () => {
      const config = createMockConfig();
      const errorMessage = 'Processing failed';
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'failed',
                error: errorMessage,
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: errorMessage,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 10000 });
    });

    it('should timeout when maxDuration is exceeded', () => {
      const config = createMockConfig({
        maxDuration: 10,
        initialDelay: 0,
        pollInterval: 5,
      });
      
      // Mock Date.now() to simulate time passing - override the beforeEach mock
      let callCount = 0;
      const originalDateNow = Date.now;
      Date.now = jest.fn(() => {
        callCount++;
        // First call sets startTime, second call checks timeout (should exceed maxDuration)
        if (callCount === 1) {
          return mockStartTime; // startTime
        }
        return mockStartTime + 11; // Exceeds maxDuration of 10
      });
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          // Delay provider handles all delay calls
          {
            call: delayProvider,
          },
          // Keep returning processing status
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'processing',
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_TIMEOUT',
          payload: {
            assetId: mockAssetId,
            message: 'Asset processing is taking longer than expected. Please check back later.',
            duration: 11, // mockStartTime + 11 - mockStartTime = 11
          },
        })
        .run({ timeout: 15000 })
        .finally(() => {
          Date.now = originalDateNow;
          callCount = 0;
        });
    });

    it('should use default values when config options are not provided', () => {
      const config = {
        type: mockAssetType,
        assetId: mockAssetId,
        onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
      };
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'completed',
                asset: mockAsset,
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_COMPLETED',
          payload: {
            assetId: mockAssetId,
            asset: mockAsset,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle missing onCompleted callback', () => {
      const config = {
        type: mockAssetType,
        assetId: mockAssetId,
        onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
      };
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'completed',
                asset: mockAsset,
              },
            },
          ],
        ])
        .not.put({ type: 'ASSET_COMPLETED' })
        .run({ timeout: 15000 });
    });

    it('should handle missing onTimeout callback', () => {
      const config = {
        type: mockAssetType,
        assetId: mockAssetId,
        maxDuration: 10,
        initialDelay: 0,
        pollInterval: 5,
        onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
      };
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'processing',
              },
            },
          ],
        ])
        .not.put({ type: 'ASSET_TIMEOUT' })
        .run({ timeout: 15000 });
    });

    it('should use custom initialDelay and pollInterval', () => {
      const config = createMockConfig({
        initialDelay: 0,
        pollInterval: 0,
      });
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'completed',
                asset: mockAsset,
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_COMPLETED',
          payload: {
            assetId: mockAssetId,
            asset: mockAsset,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle failed status with default error message', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'failed',
                // No error field
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Processing failed',
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle different asset types', () => {
      const videoConfig = createMockConfig({ type: 'video' });
      const videoAsset = {
        ...mockAsset,
        type: 'VIDEO',
        metaInfo: {
          video_file_path_preview: 'https://example.com/video-preview.jpg',
        },
      };
      
      return expectSaga(pollAssetStatus, videoConfig)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'completed',
                asset: videoAsset,
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_COMPLETED',
          payload: {
            assetId: mockAssetId,
            asset: videoAsset,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 15000 });
    });

    it('should continue polling for video assets without video_file_path_preview even when status is completed', () => {
      const videoConfig = createMockConfig({ 
        type: 'video',
        maxDuration: 50000, // Longer timeout for multiple polls
      });
      const videoAssetWithoutPreview = {
        ...mockAsset,
        type: 'VIDEO',
        metaInfo: {}, // No video_file_path_preview
      };
      const videoAssetWithPreview = {
        ...mockAsset,
        type: 'VIDEO',
        metaInfo: {
          video_file_path_preview: 'https://example.com/video-preview.jpg',
        },
      };
      
      // Create responses array that will be consumed sequentially  
      const responses = [
        {
          response: {
            status: 'completed',
            asset: videoAssetWithoutPreview,
          },
        },
        {
          response: {
            status: 'completed',
            asset: videoAssetWithPreview,
          },
        },
      ];
      
      let callIndex = 0;
      
      return expectSaga(pollAssetStatus, videoConfig)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            dynamic(() => {
              const response = responses[callIndex] || responses[responses.length - 1];
              callIndex++;
              return response;
            }),
          ],
        ])
        .put({
          type: 'ASSET_COMPLETED',
          payload: {
            assetId: mockAssetId,
            asset: videoAssetWithPreview,
            duration: 0, // Date.now() - startTime = 0 when mocked
          },
        })
        .run({ timeout: 20000 });
    });

    it('should handle API response with success: false', () => {
      const config = createMockConfig();
      const errorMessage = 'API request failed';
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              success: false,
              message: errorMessage,
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: errorMessage,
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle API response with error property', () => {
      const config = createMockConfig();
      const errorMessage = 'Network error occurred';
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              error: errorMessage,
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: errorMessage,
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle null or undefined API response', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            null,
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Failed to fetch asset status: Invalid API response',
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle API response with missing response property', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              status: 200,
              // Missing response property
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Invalid asset status response: Missing or invalid status field',
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle API response with empty response object', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {},
              // Empty response object without status
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Invalid asset status response: Missing or invalid status field',
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle API response with invalid status field (not a string)', () => {
      const config = createMockConfig();
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 200, // Invalid: status should be a string, not a number
              },
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Invalid asset status response: Missing or invalid status field',
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle API response with error message property', () => {
      const config = createMockConfig();
      const errorMessage = 'Custom error message';
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              success: false,
              message: errorMessage,
            },
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: errorMessage,
            duration: 0,
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle timeout without onTimeout callback', () => {
      const config = {
        type: mockAssetType,
        assetId: mockAssetId,
        maxDuration: 10,
        initialDelay: 0,
        pollInterval: 5,
        onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
        onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
        // No onTimeout callback
      };
      
      // Mock Date.now() to simulate time passing
      let callCount = 0;
      const originalDateNow = Date.now;
      Date.now = jest.fn(() => {
        callCount++;
        if (callCount === 1) {
          return mockStartTime; // startTime
        }
        return mockStartTime + 11; // Exceeds maxDuration of 10
      });
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            {
              response: {
                status: 'processing',
              },
            },
          ],
        ])
        .not.put({ type: 'ASSET_TIMEOUT' })
        .run({ timeout: 15000 })
        .finally(() => {
          Date.now = originalDateNow;
          callCount = 0;
        });
    });

    it('should handle catch block error when getAssetStatus throws', () => {
      const config = createMockConfig();
      const networkError = new Error('Network request failed');
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            throwError(networkError),
          ],
        ])
        .put({
          type: 'ASSET_FAILED',
          payload: {
            assetId: mockAssetId,
            error: 'Failed to check asset status: Network request failed',
          },
        })
        .run({ timeout: 15000 });
    });

    it('should handle catch block error without onFailed callback', () => {
      const config = {
        type: mockAssetType,
        assetId: mockAssetId,
        onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
        // No onFailed callback
      };
      const networkError = new Error('Network request failed');
      
      return expectSaga(pollAssetStatus, config)
        .provide({
          call: delayProvider,
        })
        .provide([
          [
            matchers.call.fn(getAssetStatus),
            throwError(networkError),
          ],
        ])
        .not.put({ type: 'ASSET_FAILED' })
        .run({ timeout: 15000 });
    });
  });
});
