import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { apiRequest, apiRequestWithRetry, _clearInflightRequests } from './api';
import { ApiRequestError } from '../types/api';
import { server } from '../test/mocks/server';
import { http, HttpResponse, delay } from 'msw';

const BASE_URL = 'http://localhost:9000/wp-json/sequensy/v1';

describe('api service', () => {
  describe('apiRequest', () => {
    describe('successful requests', () => {
      it('should make successful GET request', async () => {
        const result = await apiRequest('/triggers');
        expect(Array.isArray(result)).toBe(true);
      });

      it('should make successful POST request', async () => {
        const result = await apiRequest<{ id: number; name: string }>('/workflows', {
          method: 'POST',
          body: JSON.stringify({ name: 'Test Workflow' }),
        });

        expect(result).toHaveProperty('id');
        expect(result.name).toBe('Test Workflow');
      });

      it('should make successful PUT request', async () => {
        const result = await apiRequest<{ id: number; name: string }>('/workflows/1', {
          method: 'PUT',
          body: JSON.stringify({ name: 'Updated Workflow' }),
        });

        expect(result.id).toBe(1);
        expect(result.name).toBe('Updated Workflow');
      });

      it('should handle query parameters correctly', async () => {
        server.use(
          http.get(`${BASE_URL}/workflows`, ({ request }) => {
            const url = new URL(request.url);
            const status = url.searchParams.get('status');
            return HttpResponse.json([{ id: 1, status }]);
          }),
        );

        const result = await apiRequest<Array<{ id: number; status: string }>>(
          '/workflows?status=active',
        );
        expect(result[0].status).toBe('active');
      });
    });

    describe('headers', () => {
      it('should include nonce header', async () => {
        let capturedHeaders: Headers | null = null;

        server.use(
          http.get(`${BASE_URL}/test-headers`, ({ request }) => {
            capturedHeaders = request.headers as Headers;
            return HttpResponse.json({ success: true });
          }),
        );

        await apiRequest('/test-headers');
        expect((capturedHeaders as Headers | null)?.get('X-WP-Nonce')).toBe('test-nonce-12345');
      });

      it('should include Content-Type header', async () => {
        let capturedHeaders: Headers | null = null;

        server.use(
          http.get(`${BASE_URL}/test-content-type`, ({ request }) => {
            capturedHeaders = request.headers as Headers;
            return HttpResponse.json({ success: true });
          }),
        );

        await apiRequest('/test-content-type');
        expect((capturedHeaders as Headers | null)?.get('Content-Type')).toBe('application/json');
      });
    });

    describe('error handling', () => {
      it('should throw ApiRequestError for 401', async () => {
        server.use(
          http.get(`${BASE_URL}/protected`, () => {
            return HttpResponse.json(
              { code: 'rest_forbidden', message: 'Unauthorized' },
              { status: 401 },
            );
          }),
        );

        await expect(apiRequest('/protected')).rejects.toThrow(ApiRequestError);
        await expect(apiRequest('/protected')).rejects.toThrow('Your session has expired');
      });

      it('should throw ApiRequestError for 403', async () => {
        server.use(
          http.get(`${BASE_URL}/forbidden`, () => {
            return HttpResponse.json(
              { code: 'rest_forbidden', message: 'Forbidden' },
              { status: 403 },
            );
          }),
        );

        await expect(apiRequest('/forbidden')).rejects.toThrow('You do not have permission');
      });

      it('should throw ApiRequestError for 404', async () => {
        server.use(
          http.get(`${BASE_URL}/not-found`, () => {
            return new HttpResponse(null, { status: 404 });
          }),
        );

        await expect(apiRequest('/not-found')).rejects.toThrow('not found');
      });

      it('should throw ApiRequestError for 400 with message', async () => {
        server.use(
          http.get(`${BASE_URL}/bad-request`, () => {
            return HttpResponse.json(
              { code: 'invalid_param', message: 'Invalid parameter value' },
              { status: 400 },
            );
          }),
        );

        await expect(apiRequest('/bad-request')).rejects.toThrow('Invalid request');
      });

      it('should throw ApiRequestError for 500', async () => {
        server.use(
          http.get(`${BASE_URL}/server-error`, () => {
            return new HttpResponse(null, { status: 500 });
          }),
        );

        await expect(apiRequest('/server-error')).rejects.toThrow('An unexpected error occurred');
      });

      it('should include status code in ApiRequestError', async () => {
        server.use(
          http.get(`${BASE_URL}/error-with-status`, () => {
            return HttpResponse.json({ code: 'error', message: 'Test error' }, { status: 422 });
          }),
        );

        try {
          await apiRequest('/error-with-status');
          expect.fail('Should have thrown');
        } catch (error) {
          expect(error).toBeInstanceOf(ApiRequestError);
          expect((error as ApiRequestError).status).toBe(422);
        }
      });
    });

    describe('network errors', () => {
      it('should throw error for network failures', async () => {
        server.use(
          http.get(`${BASE_URL}/network-error`, () => {
            throw new Error('Network error');
          }),
        );

        await expect(apiRequest('/network-error')).rejects.toThrow();
      });
    });
  });

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

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

    it('should abort request after specified timeout', async () => {
      server.use(
        http.get(`${BASE_URL}/slow-endpoint`, async () => {
          // This handler will never respond because we'll abort first
          await delay('infinite');
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequest('/slow-endpoint', { timeout: 500 });

      // Attach rejection handler BEFORE advancing timers to avoid unhandled rejection warnings
      const resultPromise = promise.catch((err: Error) => err);

      // Advance time past the timeout
      await vi.advanceTimersByTimeAsync(600);

      const error = (await resultPromise) as Error;
      expect(error).toBeInstanceOf(Error);
      expect(error.message).toContain('timed out after 500ms');
    });

    it('should succeed if response arrives before timeout', async () => {
      server.use(
        http.get(`${BASE_URL}/fast-endpoint`, async () => {
          // Respond quickly
          return HttpResponse.json({ fast: true });
        }),
      );

      const promise = apiRequest('/fast-endpoint', { timeout: 5000 });
      await vi.advanceTimersByTimeAsync(100);
      const result = await promise;

      expect(result).toEqual({ fast: true });
    });

    it('should use default 30s timeout when not specified', async () => {
      server.use(
        http.get(`${BASE_URL}/default-timeout`, async () => {
          await delay('infinite');
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequest('/default-timeout');

      // Attach rejection handler BEFORE advancing timers to avoid unhandled rejection warnings
      const resultPromise = promise.catch((err: Error) => err);

      // 29 seconds should not trigger timeout
      await vi.advanceTimersByTimeAsync(29_000);

      // 31 seconds total should trigger it
      await vi.advanceTimersByTimeAsync(2_000);

      const error = await resultPromise;
      expect(error).toBeInstanceOf(Error);
      expect((error as Error).message).toContain('timed out after 30000ms');
    });

    it('should not timeout when timeout is 0', async () => {
      server.use(
        http.get(`${BASE_URL}/no-timeout`, async () => {
          return HttpResponse.json({ notimeout: true });
        }),
      );

      const promise = apiRequest('/no-timeout', { timeout: 0 });
      await vi.advanceTimersByTimeAsync(100);
      const result = await promise;

      expect(result).toEqual({ notimeout: true });
    });
  });

  describe('deduplication', () => {
    afterEach(() => {
      _clearInflightRequests();
    });

    it('should deduplicate identical in-flight GET requests', async () => {
      let fetchCount = 0;

      server.use(
        http.get(`${BASE_URL}/dedup-test`, async () => {
          fetchCount++;
          await delay(50);
          return HttpResponse.json({ data: 'result' });
        }),
      );

      // Fire two identical GET requests concurrently
      const [result1, result2] = await Promise.all([
        apiRequest('/dedup-test', { timeout: 0 }),
        apiRequest('/dedup-test', { timeout: 0 }),
      ]);

      // Both should return the same result
      expect(result1).toEqual({ data: 'result' });
      expect(result2).toEqual({ data: 'result' });

      // But only one fetch should have been made
      expect(fetchCount).toBe(1);
    });

    it('should NOT deduplicate POST requests', async () => {
      let fetchCount = 0;

      server.use(
        http.post(`${BASE_URL}/dedup-post`, async () => {
          fetchCount++;
          await delay(50);
          return HttpResponse.json({ created: true });
        }),
      );

      await Promise.all([
        apiRequest('/dedup-post', { method: 'POST', body: '{}', timeout: 0 }),
        apiRequest('/dedup-post', { method: 'POST', body: '{}', timeout: 0 }),
      ]);

      // Both requests should have been made (not deduplicated)
      expect(fetchCount).toBe(2);
    });

    it('should NOT deduplicate PUT requests', async () => {
      let fetchCount = 0;

      server.use(
        http.put(`${BASE_URL}/dedup-put`, async () => {
          fetchCount++;
          await delay(50);
          return HttpResponse.json({ updated: fetchCount });
        }),
      );

      await Promise.all([
        apiRequest('/dedup-put', { method: 'PUT', body: '{}', timeout: 0 }),
        apiRequest('/dedup-put', { method: 'PUT', body: '{}', timeout: 0 }),
      ]);

      expect(fetchCount).toBe(2);
    });

    it('should NOT deduplicate DELETE requests', async () => {
      let fetchCount = 0;

      server.use(
        http.delete(`${BASE_URL}/dedup-delete`, async () => {
          fetchCount++;
          await delay(50);
          return HttpResponse.json({ deleted: fetchCount });
        }),
      );

      await Promise.all([
        apiRequest('/dedup-delete', { method: 'DELETE', timeout: 0 }),
        apiRequest('/dedup-delete', { method: 'DELETE', timeout: 0 }),
      ]);

      expect(fetchCount).toBe(2);
    });

    it('should allow new GET request after previous one completes', async () => {
      let fetchCount = 0;

      server.use(
        http.get(`${BASE_URL}/sequential-dedup`, async () => {
          fetchCount++;
          return HttpResponse.json({ count: fetchCount });
        }),
      );

      // First request
      const result1 = await apiRequest('/sequential-dedup', { timeout: 0 });
      expect(result1).toEqual({ count: 1 });

      // Second request after first completes - should make a new fetch
      const result2 = await apiRequest('/sequential-dedup', { timeout: 0 });
      expect(result2).toEqual({ count: 2 });

      expect(fetchCount).toBe(2);
    });

    it('should deduplicate GET requests to different endpoints independently', async () => {
      let fetchCountA = 0;
      let fetchCountB = 0;

      server.use(
        http.get(`${BASE_URL}/endpoint-a`, async () => {
          fetchCountA++;
          await delay(50);
          return HttpResponse.json({ endpoint: 'a' });
        }),
        http.get(`${BASE_URL}/endpoint-b`, async () => {
          fetchCountB++;
          await delay(50);
          return HttpResponse.json({ endpoint: 'b' });
        }),
      );

      const [resultA1, resultA2, resultB1, resultB2] = await Promise.all([
        apiRequest('/endpoint-a', { timeout: 0 }),
        apiRequest('/endpoint-a', { timeout: 0 }),
        apiRequest('/endpoint-b', { timeout: 0 }),
        apiRequest('/endpoint-b', { timeout: 0 }),
      ]);

      // Each endpoint should only trigger one fetch
      expect(fetchCountA).toBe(1);
      expect(fetchCountB).toBe(1);

      expect(resultA1).toEqual({ endpoint: 'a' });
      expect(resultA2).toEqual({ endpoint: 'a' });
      expect(resultB1).toEqual({ endpoint: 'b' });
      expect(resultB2).toEqual({ endpoint: 'b' });
    });

    it('should propagate errors to all deduplicated callers', async () => {
      server.use(
        http.get(`${BASE_URL}/dedup-error`, async () => {
          await delay(50);
          return HttpResponse.json({ code: 'not_found', message: 'Not found' }, { status: 404 });
        }),
      );

      const promise1 = apiRequest('/dedup-error', { timeout: 0 });
      const promise2 = apiRequest('/dedup-error', { timeout: 0 });

      // Both should reject with the same error
      await expect(promise1).rejects.toThrow('not found');
      await expect(promise2).rejects.toThrow('not found');
    });

    it('should clean up inflight map after request completes', async () => {
      server.use(
        http.get(`${BASE_URL}/cleanup-test`, async () => {
          return HttpResponse.json({ success: true });
        }),
      );

      // Complete a request
      await apiRequest('/cleanup-test', { timeout: 0 });

      // Subsequent request should trigger a new fetch (not use stale promise)
      let fetchCount = 0;
      server.use(
        http.get(`${BASE_URL}/cleanup-test`, async () => {
          fetchCount++;
          return HttpResponse.json({ fresh: true });
        }),
      );

      const result = await apiRequest('/cleanup-test', { timeout: 0 });
      expect(result).toEqual({ fresh: true });
      expect(fetchCount).toBe(1);
    });
  });

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

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

    it('should succeed on first try', async () => {
      const promise = apiRequestWithRetry('/triggers', { timeout: 0 });
      await vi.runAllTimersAsync();
      const result = await promise;
      expect(Array.isArray(result)).toBe(true);
    });

    it('should retry on 500 error and eventually succeed', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/flaky`, () => {
          attempts++;
          if (attempts < 2) {
            return new HttpResponse(null, { status: 500 });
          }
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequestWithRetry('/flaky', { timeout: 0 }, { maxRetries: 3 });
      await vi.runAllTimersAsync();
      const result = await promise;

      expect(result).toEqual({ success: true });
      expect(attempts).toBe(2);
    });

    it('should retry on 503 error', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/service-unavailable`, () => {
          attempts++;
          if (attempts < 2) {
            return new HttpResponse(null, { status: 503 });
          }
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequestWithRetry(
        '/service-unavailable',
        { timeout: 0 },
        { maxRetries: 2 },
      );
      await vi.runAllTimersAsync();
      const result = await promise;

      expect(result).toEqual({ success: true });
    });

    it('should not retry on 400 error (non-retryable)', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/bad-request-no-retry`, () => {
          attempts++;
          return HttpResponse.json(
            { code: 'invalid_param', message: 'Bad request' },
            { status: 400 },
          );
        }),
      );

      await expect(
        apiRequestWithRetry('/bad-request-no-retry', { timeout: 0 }, { maxRetries: 3 }),
      ).rejects.toThrow();
      expect(attempts).toBe(1); // No retries for 400
    });

    it('should not retry on 401 error (non-retryable)', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/unauthorized-no-retry`, () => {
          attempts++;
          return HttpResponse.json(
            { code: 'rest_forbidden', message: 'Unauthorized' },
            { status: 401 },
          );
        }),
      );

      await expect(
        apiRequestWithRetry('/unauthorized-no-retry', { timeout: 0 }, { maxRetries: 3 }),
      ).rejects.toThrow('Your session has expired');
      expect(attempts).toBe(1);
    });

    it('should not retry on 403 error (non-retryable)', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/forbidden-no-retry`, () => {
          attempts++;
          return HttpResponse.json(
            { code: 'rest_forbidden', message: 'Forbidden' },
            { status: 403 },
          );
        }),
      );

      await expect(
        apiRequestWithRetry('/forbidden-no-retry', { timeout: 0 }, { maxRetries: 3 }),
      ).rejects.toThrow('You do not have permission');
      expect(attempts).toBe(1);
    });

    it('should retry on 429 rate limit error', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/rate-limited`, () => {
          attempts++;
          if (attempts < 2) {
            return new HttpResponse(null, { status: 429 });
          }
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequestWithRetry('/rate-limited', { timeout: 0 }, { maxRetries: 2 });
      await vi.runAllTimersAsync();
      const result = await promise;

      expect(result).toEqual({ success: true });
      expect(attempts).toBe(2);
    });

    it('should exhaust retries and throw last error', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/always-fails`, () => {
          attempts++;
          return new HttpResponse(null, { status: 503 });
        }),
      );

      // For retry tests that exhaust, we need to handle the promise properly
      // by catching and verifying the error
      let caughtError: Error | null = null;
      const promise = apiRequestWithRetry('/always-fails', { timeout: 0 }, { maxRetries: 2 }).catch(
        (err) => {
          caughtError = err;
        },
      );

      // Run timers to allow retries to complete
      await vi.runAllTimersAsync();
      await promise;

      expect(caughtError).not.toBeNull();
      expect(attempts).toBe(3); // Initial + 2 retries
    });

    it('should use custom retry config', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/custom-retry`, () => {
          attempts++;
          if (attempts < 4) {
            return new HttpResponse(null, { status: 500 });
          }
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequestWithRetry(
        '/custom-retry',
        { timeout: 0 },
        { maxRetries: 5, baseDelay: 100, maxDelay: 1000 },
      );
      await vi.runAllTimersAsync();
      const result = await promise;

      expect(result).toEqual({ success: true });
      expect(attempts).toBe(4);
    });

    it('should use default retry config when not specified', async () => {
      let attempts = 0;

      server.use(
        http.get(`${BASE_URL}/default-retry`, () => {
          attempts++;
          if (attempts < 3) {
            return new HttpResponse(null, { status: 500 });
          }
          return HttpResponse.json({ success: true });
        }),
      );

      const promise = apiRequestWithRetry('/default-retry', { timeout: 0 });
      await vi.runAllTimersAsync();
      const result = await promise;

      expect(result).toEqual({ success: true });
      expect(attempts).toBe(3);
    });
  });
});
