/**
 * Retry Utility Tests
 *
 * Tests for retry logic with exponential backoff.
 */

import { describe, it, expect, vi } from "vitest";
import { AxiosError } from "axios";
import { withRetry, isRetryableError } from "./retry";

describe("Retry Utility", () => {
  describe("isRetryableError", () => {
    it("should return true for ECONNREFUSED error", () => {
      const error: Partial<AxiosError> = {
        code: "ECONNREFUSED",
        message: "Connection refused",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(true);
    });

    it("should return true for ETIMEDOUT error", () => {
      const error: Partial<AxiosError> = {
        code: "ETIMEDOUT",
        message: "Connection timed out",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(true);
    });

    it("should return true for 5xx server errors", () => {
      const error: Partial<AxiosError> = {
        response: {
          status: 500,
        } as never,
        message: "Internal Server Error",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(true);
    });

    it("should return true for 429 Too Many Requests", () => {
      const error: Partial<AxiosError> = {
        response: {
          status: 429,
        } as never,
        message: "Too Many Requests",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(true);
    });

    it("should return false for 4xx client errors", () => {
      const error: Partial<AxiosError> = {
        response: {
          status: 404,
        } as never,
        message: "Not Found",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(false);
    });

    it("should return false for 400 Bad Request", () => {
      const error: Partial<AxiosError> = {
        response: {
          status: 400,
        } as never,
        message: "Bad Request",
        name: "Error",
      };

      expect(isRetryableError(error)).toBe(false);
    });

    it("should return false for non-Error objects", () => {
      expect(isRetryableError("not an error")).toBe(false);
      expect(isRetryableError(null)).toBe(false);
      expect(isRetryableError(undefined)).toBe(false);
    });
  });

  describe("withRetry", () => {
    it("should succeed on first attempt", async () => {
      const operation = vi.fn().mockResolvedValue("success");

      const result = await withRetry(operation);

      expect(result).toBe("success");
      expect(operation).toHaveBeenCalledTimes(1);
    });

    it("should retry on retryable error and eventually succeed", async () => {
      const operation = vi
        .fn()
        .mockRejectedValueOnce({
          code: "ECONNREFUSED",
          message: "Connection refused",
          name: "Error",
        })
        .mockRejectedValueOnce({
          code: "ETIMEDOUT",
          message: "Timeout",
          name: "Error",
        })
        .mockResolvedValue("success");

      // Use shorter delays for testing
      const result = await withRetry(operation, {
        initialDelay: 10,
        maxDelay: 40,
      });

      expect(result).toBe("success");
      expect(operation).toHaveBeenCalledTimes(3);
    });

    it("should not retry on 4xx client errors", async () => {
      const error: Partial<AxiosError> = {
        response: {
          status: 400,
        } as never,
        message: "Bad Request",
        name: "Error",
      };

      const operation = vi.fn().mockRejectedValue(error);

      await expect(withRetry(operation)).rejects.toMatchObject({
        message: "Bad Request",
      });

      expect(operation).toHaveBeenCalledTimes(1);
    });

    it("should throw error after max retries", async () => {
      const error: Partial<AxiosError> = {
        code: "ECONNREFUSED",
        message: "Connection refused",
        name: "Error",
      };

      const operation = vi.fn().mockRejectedValue(error);

      await expect(
        withRetry(operation, {
          maxRetries: 3,
          initialDelay: 10,
        })
      ).rejects.toMatchObject({
        message: "Connection refused",
      });

      // Should be called 4 times (initial + 3 retries)
      expect(operation).toHaveBeenCalledTimes(4);
    });

    it("should verify exponential backoff timing", async () => {
      const operation = vi.fn().mockRejectedValue({
        code: "ECONNREFUSED",
        message: "Connection refused",
        name: "Error",
      });

      // Verify the function attempts multiple retries and eventually throws
      await expect(
        withRetry(operation, {
          maxRetries: 2,
          initialDelay: 10,
        })
      ).rejects.toMatchObject({
        message: "Connection refused",
      });

      // Should be called 3 times (initial + 2 retries)
      expect(operation).toHaveBeenCalledTimes(3);
    });
  });
});
