import { describe, test, expect, mock } from "bun:test";
import { withCwRetry } from "../../src/modules/cw-utils/withCwRetry";

describe("withCwRetry", () => {
  // -------------------------------------------------------------------
  // Successful execution
  // -------------------------------------------------------------------
  test("returns result on first successful call", async () => {
    const fn = mock(() => Promise.resolve("ok"));
    const result = await withCwRetry(fn);
    expect(result).toBe("ok");
    expect(fn).toHaveBeenCalledTimes(1);
  });

  test("returns result when call succeeds after transient failure", async () => {
    let attempt = 0;
    const fn = mock(() => {
      attempt++;
      if (attempt === 1) {
        const err: any = new Error("timeout");
        err.isAxiosError = true;
        err.code = "ETIMEDOUT";
        return Promise.reject(err);
      }
      return Promise.resolve("recovered");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("recovered");
    expect(fn).toHaveBeenCalledTimes(2);
  });

  // -------------------------------------------------------------------
  // Retry on transient errors
  // -------------------------------------------------------------------
  test("retries on ECONNABORTED", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls < 3) {
        const err: any = new Error("aborted");
        err.isAxiosError = true;
        err.code = "ECONNABORTED";
        return Promise.reject(err);
      }
      return Promise.resolve("done");
    });
    const result = await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 });
    expect(result).toBe("done");
    expect(fn).toHaveBeenCalledTimes(3);
  });

  test("retries on ECONNRESET", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("reset");
        err.isAxiosError = true;
        err.code = "ECONNRESET";
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  test("retries on ECONNREFUSED", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("refused");
        err.isAxiosError = true;
        err.code = "ECONNREFUSED";
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  test("retries on ERR_NETWORK", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("network");
        err.isAxiosError = true;
        err.code = "ERR_NETWORK";
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  test("retries on ENETUNREACH", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("unreachable");
        err.isAxiosError = true;
        err.code = "ENETUNREACH";
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  test("retries on 5xx server errors", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("server error");
        err.isAxiosError = true;
        err.response = { status: 502 };
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  test("retries on 500 status", async () => {
    let calls = 0;
    const fn = mock(() => {
      calls++;
      if (calls === 1) {
        const err: any = new Error("internal");
        err.isAxiosError = true;
        err.response = { status: 500 };
        return Promise.reject(err);
      }
      return Promise.resolve("ok");
    });
    const result = await withCwRetry(fn, { baseDelayMs: 1 });
    expect(result).toBe("ok");
  });

  // -------------------------------------------------------------------
  // Non-retryable errors
  // -------------------------------------------------------------------
  test("does not retry on 4xx errors", async () => {
    const err: any = new Error("not found");
    err.isAxiosError = true;
    err.response = { status: 404 };
    const fn = mock(() => Promise.reject(err));
    await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(1);
  });

  test("does not retry on 400 errors", async () => {
    const err: any = new Error("bad request");
    err.isAxiosError = true;
    err.response = { status: 400 };
    const fn = mock(() => Promise.reject(err));
    await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(1);
  });

  test("does not retry on non-Axios errors", async () => {
    const fn = mock(() => Promise.reject(new Error("generic")));
    await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow(
      "generic",
    );
    expect(fn).toHaveBeenCalledTimes(1);
  });

  test("does not retry on non-object errors", async () => {
    const fn = mock(() => Promise.reject("string error"));
    await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toBe(
      "string error",
    );
    expect(fn).toHaveBeenCalledTimes(1);
  });

  // -------------------------------------------------------------------
  // Max attempts exhausted
  // -------------------------------------------------------------------
  test("throws after maxAttempts exhausted", async () => {
    const err: any = new Error("timeout");
    err.isAxiosError = true;
    err.code = "ETIMEDOUT";
    const fn = mock(() => Promise.reject(err));
    await expect(
      withCwRetry(fn, { maxAttempts: 2, baseDelayMs: 1 }),
    ).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(2);
  });

  test("throws the last error when retries exhausted", async () => {
    const err: any = new Error("persistent timeout");
    err.isAxiosError = true;
    err.code = "ETIMEDOUT";
    const fn = mock(() => Promise.reject(err));
    try {
      await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 });
      expect(true).toBe(false); // should not reach
    } catch (e: any) {
      expect(e.message).toBe("persistent timeout");
    }
  });

  // -------------------------------------------------------------------
  // Options
  // -------------------------------------------------------------------
  test("defaults to 3 maxAttempts", async () => {
    const err: any = new Error("timeout");
    err.isAxiosError = true;
    err.code = "ETIMEDOUT";
    const fn = mock(() => Promise.reject(err));
    await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(3);
  });

  test("accepts custom maxAttempts", async () => {
    const err: any = new Error("timeout");
    err.isAxiosError = true;
    err.code = "ETIMEDOUT";
    const fn = mock(() => Promise.reject(err));
    await expect(
      withCwRetry(fn, { maxAttempts: 5, baseDelayMs: 1 }),
    ).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(5);
  });

  test("maxAttempts of 1 means no retries", async () => {
    const err: any = new Error("timeout");
    err.isAxiosError = true;
    err.code = "ETIMEDOUT";
    const fn = mock(() => Promise.reject(err));
    await expect(
      withCwRetry(fn, { maxAttempts: 1, baseDelayMs: 1 }),
    ).rejects.toThrow();
    expect(fn).toHaveBeenCalledTimes(1);
  });
});
