/**
 * Copyright (c) 2023-present Pi Dash Software, Inc. and contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 * See the LICENSE file for details.
 */

import { describe, it, expect, assert } from "vitest";
import { Effect, Duration, Either } from "effect";
import { withTimeoutAndRetry, recoverWithDefault, tryAsync } from "@/services/pdf-export/effect-utils";
import { PdfTimeoutError } from "@/schema/pdf-export";

describe("effect-utils", () => {
  describe("withTimeoutAndRetry", () => {
    it("should succeed when effect completes within timeout", async () => {
      const effect = Effect.succeed("success");
      const wrapped = withTimeoutAndRetry("test-operation")(effect);

      const result = await Effect.runPromise(wrapped);
      expect(result).toBe("success");
    });

    it("should fail with PdfTimeoutError when effect exceeds timeout", async () => {
      const slowEffect = Effect.gen(function* () {
        yield* Effect.sleep(Duration.millis(500));
        return "success";
      });

      const wrapped = withTimeoutAndRetry("test-operation", {
        timeoutMs: 50,
        maxRetries: 0,
      })(slowEffect);

      const result = await Effect.runPromise(Effect.either(wrapped));

      assert(Either.isLeft(result), "Expected Left but got Right");
      expect(result.left).toBeInstanceOf(PdfTimeoutError);
      expect((result.left as PdfTimeoutError).operation).toBe("test-operation");
    });

    it("should retry on failure up to maxRetries times", async () => {
      const attemptCounter = { count: 0 };

      const flakyEffect = Effect.gen(function* () {
        attemptCounter.count++;
        if (attemptCounter.count < 3) {
          return yield* Effect.fail(new Error("transient failure"));
        }
        return "success";
      });

      const wrapped = withTimeoutAndRetry("test-operation", {
        timeoutMs: 5000,
        maxRetries: 3,
      })(flakyEffect);

      const result = await Effect.runPromise(wrapped);

      expect(result).toBe("success");
      expect(attemptCounter.count).toBe(3);
    });

    it("should fail after exhausting retries", async () => {
      const effect = Effect.fail(new Error("permanent failure"));

      const wrapped = withTimeoutAndRetry("test-operation", {
        timeoutMs: 5000,
        maxRetries: 2,
      })(effect);

      const result = await Effect.runPromise(Effect.either(wrapped));

      expect(result._tag).toBe("Left");
    });
  });

  describe("recoverWithDefault", () => {
    it("should return success value when effect succeeds", async () => {
      const effect = Effect.succeed("success");
      const wrapped = recoverWithDefault("fallback")(effect);

      const result = await Effect.runPromise(wrapped);
      expect(result).toBe("success");
    });

    it("should return fallback value when effect fails", async () => {
      const effect = Effect.fail(new Error("failure"));
      const wrapped = recoverWithDefault("fallback")(effect);

      const result = await Effect.runPromise(wrapped);
      expect(result).toBe("fallback");
    });

    it("should log warning when recovering from error", async () => {
      const logs: string[] = [];

      const effect = Effect.fail(new Error("test error")).pipe(
        recoverWithDefault("fallback"),
        Effect.tap(() => Effect.sync(() => logs.push("after recovery")))
      );

      const result = await Effect.runPromise(effect);

      expect(result).toBe("fallback");
      expect(logs).toContain("after recovery");
    });

    it("should work with complex fallback objects", async () => {
      const fallback = { items: [], count: 0, metadata: { version: 1 } };

      const effect = Effect.fail(new Error("failure"));
      const wrapped = recoverWithDefault(fallback)(effect);

      const result = await Effect.runPromise(wrapped);
      expect(result).toEqual(fallback);
    });
  });

  describe("tryAsync", () => {
    it("should wrap successful promise", async () => {
      const effect = tryAsync(
        () => Promise.resolve("success"),
        (err) => new Error(`wrapped: ${err}`)
      );

      const result = await Effect.runPromise(effect);
      expect(result).toBe("success");
    });

    it("should wrap rejected promise with custom error", async () => {
      const effect = tryAsync(
        () => Promise.reject(new Error("original")),
        (err) => new Error(`wrapped: ${(err as Error).message}`)
      );

      const result = await Effect.runPromise(Effect.either(effect));

      assert(Either.isLeft(result), "Expected Left but got Right");
      expect(result.left.message).toBe("wrapped: original");
    });

    it("should handle synchronous throws", async () => {
      const effect = tryAsync(
        () => {
          throw new Error("sync error");
        },
        (err) => new Error(`caught: ${(err as Error).message}`)
      );

      const result = await Effect.runPromise(Effect.either(effect));

      assert(Either.isLeft(result), "Expected Left but got Right");
      expect(result.left.message).toBe("caught: sync error");
    });
  });
});
