import { Alepha } from "alepha";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { RetryCancelError } from "../errors/RetryCancelError.ts";
import { RetryTimeoutError } from "../errors/RetryTimeoutError.ts";
import { RetryProvider } from "../providers/RetryProvider.ts";

describe("RetryProvider", () => {
  let alepha: Alepha;
  let retryProvider: RetryProvider;

  beforeEach(async () => {
    alepha = Alepha.create();
    retryProvider = alepha.inject(RetryProvider);
    await alepha.start();
  });

  test("should retry handler up to max retries", async () => {
    let attempts = 0;
    const handler = vi.fn(() => {
      attempts++;
      if (attempts < 3) {
        throw new Error("Retry");
      }
      return "success";
    });

    const result = await retryProvider.retry({ handler, max: 3 });

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

  test("should throw error after max retries exceeded", async () => {
    const handler = vi.fn(() => {
      throw new Error("Always fails");
    });

    await expect(retryProvider.retry({ handler, max: 3 })).rejects.toThrowError(
      "Always fails",
    );

    expect(handler).toHaveBeenCalledTimes(3);
  });

  test("should only retry when condition matches", async () => {
    let attempts = 0;
    const handler = vi.fn(() => {
      attempts++;
      throw new Error(`Error${attempts}`);
    });

    const when = (error: Error) => error.message === "Error1";

    await expect(
      retryProvider.retry({ handler, max: 10, when }),
    ).rejects.toThrowError("Error2");

    // Should fail on second attempt because when() returns false
    expect(handler).toHaveBeenCalledTimes(2);
  });

  test("should use fixed backoff delay", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const startTime = Date.now();

    await expect(
      retryProvider.retry({ handler, max: 3, backoff: 100 }),
    ).rejects.toThrowError("Retry");

    const duration = Date.now() - startTime;

    // Should have waited ~200ms total (2 retries × 100ms)
    expect(duration).toBeGreaterThanOrEqual(190);
    expect(duration).toBeLessThan(300);
  });

  test("should use exponential backoff with jitter", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const startTime = Date.now();

    await expect(
      retryProvider.retry({
        handler,
        max: 3,
        backoff: { initial: 100, factor: 2, jitter: true },
      }),
    ).rejects.toThrowError("Retry");

    const duration = Date.now() - startTime;

    // First retry: ~100ms, Second retry: ~200ms
    // With jitter, expect at least base delays
    expect(duration).toBeGreaterThanOrEqual(280);
  });

  test("should respect maxDuration timeout", { retry: 3 }, async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    await expect(
      retryProvider.retry({
        handler,
        max: 10,
        maxDuration: [100, "milliseconds"],
        backoff: 50,
      }),
    ).rejects.toThrowError(RetryTimeoutError);

    // Should not reach max retries due to timeout
    expect(handler).toHaveBeenCalledTimes(2);
  });

  test("should respect AbortSignal cancellation", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const abortController = new AbortController();

    // Abort after first attempt
    setTimeout(() => abortController.abort(), 50);

    await expect(
      retryProvider.retry({
        handler,
        max: 10,
        backoff: 100,
        signal: abortController.signal,
      }),
    ).rejects.toThrowError(RetryCancelError);

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

  test("should call onError callback on each failure", async () => {
    let attempts = 0;
    const handler = vi.fn(() => {
      attempts++;
      if (attempts < 3) {
        throw new Error(`Attempt ${attempts}`);
      }
      return "success";
    });

    const onError = vi.fn();

    await retryProvider.retry({ handler, max: 3, onError });

    expect(onError).toHaveBeenCalledTimes(2);
    expect(onError).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ message: "Attempt 1" }),
      1,
    );
    expect(onError).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ message: "Attempt 2" }),
      2,
    );
  });

  test("should pass arguments to handler", async () => {
    const handler = vi.fn((a: number, b: string) => {
      return `${a}-${b}`;
    });

    const result = await retryProvider.retry({ handler, max: 1 }, 42, "test");

    expect(result).toBe("42-test");
    expect(handler).toHaveBeenCalledWith(42, "test");
  });

  test("should log warnings on retry failures", async () => {
    const handler = vi.fn(() => {
      throw new Error("Test error");
    });

    // Spy on the log method
    // @ts-expect-error - accessing protected property for testing
    const logSpy = vi.spyOn(retryProvider.log, "warn");

    await expect(retryProvider.retry({ handler, max: 2 })).rejects.toThrowError(
      "Test error",
    );

    expect(logSpy).toHaveBeenCalledTimes(2);
    expect(logSpy).toHaveBeenCalledWith(
      "Retry attempt failed",
      expect.objectContaining({
        attempt: 1,
        maxAttempts: 2,
        remainingAttempts: 1,
        error: "Test error",
        errorName: "Error",
      }),
    );
  });

  test("should support additionalSignal for combined cancellation", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const additionalController = new AbortController();

    // Abort the additional signal
    setTimeout(() => additionalController.abort(), 50);

    await expect(
      retryProvider.retry({
        handler,
        max: 10,
        backoff: 100,
        additionalSignal: additionalController.signal,
      }),
    ).rejects.toThrowError(RetryCancelError);

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

  test("should not retry on non-Error throws", async () => {
    const handler = vi.fn(() => {
      throw "string error";
    });

    await expect(retryProvider.retry({ handler, max: 3 })).rejects.toBe(
      "string error",
    );

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

  test("should succeed on first attempt without retries", async () => {
    const handler = vi.fn(() => "immediate success");

    const result = await retryProvider.retry({ handler, max: 3 });

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

  test("should use default max of 3 when not specified", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    await expect(retryProvider.retry({ handler })).rejects.toThrowError(
      "Retry",
    );

    expect(handler).toHaveBeenCalledTimes(3);
  });

  // Bug #1 (Red/Critical): Signal precedence issue
  // When both signal and additionalSignal are provided, ONLY signal is respected during backoff
  // This means additionalSignal (app lifecycle) cannot cancel during backoff if user signal exists
  test("should respect both user signal and additionalSignal during backoff waits", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const userController = new AbortController();
    const additionalController = new AbortController();

    // Start retry with both signals provided
    const promise = retryProvider.retry({
      handler,
      max: 10,
      backoff: 200, // Long backoff to ensure we have time to abort
      signal: userController.signal,
      additionalSignal: additionalController.signal,
    });

    // Wait for first attempt to fail and backoff to start
    await new Promise((resolve) => setTimeout(resolve, 10));

    // Abort additionalSignal during backoff (user signal still active)
    additionalController.abort();

    // BUG: This currently does NOT throw RetryCancelError because line 169 only uses signal
    // The additionalSignal abort is ignored during wait
    // EXPECTED: Should throw RetryCancelError
    // ACTUAL: Continues retrying until user signal aborts
    const startTime = Date.now();

    try {
      await promise;
      // If we reach here, the bug exists - additionalSignal was ignored
      throw new Error("Should have thrown RetryCancelError");
    } catch (error) {
      const duration = Date.now() - startTime;

      // If bug exists: continues for ~200ms+ (full backoff)
      // If fixed: cancels immediately (~10-30ms)
      if (error instanceof RetryCancelError) {
        // Fixed: should cancel quickly when additionalSignal aborts
        expect(duration).toBeLessThan(100);
      } else {
        // Bug exists: keeps waiting for full backoff despite additionalSignal abort
        expect(duration).toBeGreaterThanOrEqual(180);
      }
    }
  });

  // Bug #2 (Yellow/High): Abort race condition
  test("should throw RetryCancelError when aborted after handler error", async () => {
    let attempts = 0;
    const handler = vi.fn(() => {
      attempts++;
      if (attempts === 1) {
        throw new Error("Handler failed");
      }
      throw new Error("Should not reach here");
    });

    const abortController = new AbortController();

    // Abort immediately after first error (before backoff wait)
    setTimeout(() => abortController.abort(), 10);

    await expect(
      retryProvider.retry({
        handler,
        max: 10,
        backoff: 100,
        signal: abortController.signal,
      }),
    ).rejects.toThrowError(RetryCancelError);

    // Should have attempted once, then aborted
    expect(handler).toHaveBeenCalledTimes(1);
  });

  // Bug #3 (Yellow/High): Timeout check timing
  test("should not allow handler to complete after maxDuration", async () => {
    const handler = vi.fn(async () => {
      // Handler takes 150ms
      await new Promise((resolve) => setTimeout(resolve, 150));
      throw new Error("Retry");
    });

    const startTime = Date.now();

    await expect(
      retryProvider.retry({
        handler,
        max: 10,
        maxDuration: [100, "milliseconds"],
        backoff: 10,
      }),
    ).rejects.toThrowError(RetryTimeoutError);

    const duration = Date.now() - startTime;

    // Should timeout around 100ms, not wait for handler to complete (150ms)
    // With current bug, this will fail because handler completes at ~150ms
    expect(duration).toBeLessThan(200);
    expect(handler).toHaveBeenCalledTimes(1);
  });

  // Bug #7 (Medium): onError not called on final attempt
  test("should call onError on the final failed attempt", async () => {
    const handler = vi.fn(() => {
      throw new Error("Always fails");
    });

    const onError = vi.fn();

    await expect(
      retryProvider.retry({
        handler,
        max: 3,
        backoff: 0,
        onError,
      }),
    ).rejects.toThrowError("Always fails");

    // Should call onError for ALL attempts, including the final one
    // BUG: Currently only called 2 times (attempts 1-2), not 3 times
    expect(onError).toHaveBeenCalledTimes(3);
    expect(onError).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ message: "Always fails" }),
      1,
    );
    expect(onError).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ message: "Always fails" }),
      2,
    );
    expect(onError).toHaveBeenNthCalledWith(
      3,
      expect.objectContaining({ message: "Always fails" }),
      3,
    );
  });

  // Bug #8 (Critical): AbortSignal.any() memory leak
  test("should not create multiple AbortSignal instances during retries", async () => {
    const handler = vi.fn(() => {
      throw new Error("Retry");
    });

    const userController = new AbortController();
    const additionalController = new AbortController();

    // Track how many times AbortSignal.any is called
    const originalAny = AbortSignal.any;
    const anySpy = vi.fn(originalAny);
    AbortSignal.any = anySpy as any;

    try {
      await expect(
        retryProvider.retry({
          handler,
          max: 5,
          backoff: 10,
          signal: userController.signal,
          additionalSignal: additionalController.signal,
        }),
      ).rejects.toThrowError("Retry");

      // BUG: Currently creates a NEW signal for each backoff (4 times for 5 attempts)
      // EXPECTED: Should only create ONE combined signal at the start
      expect(anySpy).toHaveBeenCalledTimes(1);
    } finally {
      // Restore original
      AbortSignal.any = originalAny;
    }
  });
});
