import gensync from "gensync";
import { makeStrongCacheSync, makeStrongCache } from "../lib/config/caching.js";
import { waitFor } from "../lib/gensync-utils/async.js";

describe("caching API", () => {
  it("should allow permacaching with .forever()", () => {
    let count = 0;

    const fn = makeStrongCacheSync((arg, cache) => {
      cache.forever();
      return { arg, count: count++ };
    });

    expect(fn("one")).toEqual({ arg: "one", count: 0 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", count: 1 });
    expect(fn("two")).toBe(fn("two"));

    expect(fn("one")).not.toEqual(fn("two"));
  });

  it("should allow disabling caching with .never()", () => {
    let count = 0;

    const fn = makeStrongCacheSync((arg, cache) => {
      cache.never();
      return { arg, count: count++ };
    });

    expect(fn("one")).toEqual({ arg: "one", count: 0 });
    expect(fn("one")).toEqual({ arg: "one", count: 1 });
    expect(fn("one")).not.toEqual(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", count: 4 });
    expect(fn("two")).toEqual({ arg: "two", count: 5 });
    expect(fn("two")).not.toEqual(fn("two"));

    expect(fn("one")).not.toEqual(fn("two"));
  });

  it("should allow caching based on a value with .using(fn)", () => {
    let count = 0;
    let other = "default";

    const fn = makeStrongCacheSync((arg, cache) => {
      const val = cache.using(() => other);

      return { arg, val, count: count++ };
    });

    expect(fn("one")).toEqual({ arg: "one", val: "default", count: 0 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "default", count: 1 });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({ arg: "one", val: "new", count: 2 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "new", count: 3 });
    expect(fn("two")).toBe(fn("two"));

    other = "default";

    expect(fn("one")).toEqual({ arg: "one", val: "default", count: 0 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "default", count: 1 });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({ arg: "one", val: "new", count: 2 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "new", count: 3 });
    expect(fn("two")).toBe(fn("two"));
  });

  it("should allow invalidation based on a value with .invalidate(fn)", () => {
    let count = 0;
    let other = "default";

    const fn = makeStrongCacheSync((arg, cache) => {
      const val = cache.invalidate(() => other);

      return { arg, val, count: count++ };
    });

    expect(fn("one")).toEqual({ arg: "one", val: "default", count: 0 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "default", count: 1 });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({ arg: "one", val: "new", count: 2 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "new", count: 3 });
    expect(fn("two")).toBe(fn("two"));

    other = "default";

    expect(fn("one")).toEqual({ arg: "one", val: "default", count: 4 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "default", count: 5 });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({ arg: "one", val: "new", count: 6 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", val: "new", count: 7 });
    expect(fn("two")).toBe(fn("two"));
  });

  it("should allow invalidation with .using and .invalidate", () => {
    let count = 0;
    let other = "default";
    let another = "another";

    const fn = makeStrongCacheSync((arg, cache) => {
      const val = cache.using(() => other);
      const val2 = cache.invalidate(() => another);

      return { arg, val, val2, count: count++ };
    });

    expect(fn("one")).toEqual({
      arg: "one",
      val: "default",
      val2: "another",
      count: 0,
    });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({
      arg: "two",
      val: "default",
      val2: "another",
      count: 1,
    });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({
      arg: "one",
      val: "new",
      val2: "another",
      count: 2,
    });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({
      arg: "two",
      val: "new",
      val2: "another",
      count: 3,
    });
    expect(fn("two")).toBe(fn("two"));

    other = "default";

    expect(fn("one")).toEqual({
      arg: "one",
      val: "default",
      val2: "another",
      count: 4,
    });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({
      arg: "two",
      val: "default",
      val2: "another",
      count: 5,
    });
    expect(fn("two")).toBe(fn("two"));

    other = "new";

    expect(fn("one")).toEqual({
      arg: "one",
      val: "new",
      val2: "another",
      count: 6,
    });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({
      arg: "two",
      val: "new",
      val2: "another",
      count: 7,
    });
    expect(fn("two")).toBe(fn("two"));

    another = "second";

    expect(fn("one")).toEqual({
      arg: "one",
      val: "new",
      val2: "second",
      count: 8,
    });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({
      arg: "two",
      val: "new",
      val2: "second",
      count: 9,
    });
    expect(fn("two")).toBe(fn("two"));
  });

  it("should auto-permacache by default", () => {
    let count = 0;

    const fn = makeStrongCacheSync(arg => ({ arg, count: count++ }));

    expect(fn("one")).toEqual({ arg: "one", count: 0 });
    expect(fn("one")).toBe(fn("one"));

    expect(fn("two")).toEqual({ arg: "two", count: 1 });
    expect(fn("two")).toBe(fn("two"));

    expect(fn("one")).not.toEqual(fn("two"));
  });

  it("should throw if you set permacaching and use .using", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.forever();

      cache.using(() => null);
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you set permacaching and use .invalidate", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.forever();

      cache.invalidate(() => null);
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you set permacaching and use .never", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.forever();

      cache.never();
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you set no caching and use .using", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.never();

      cache.using(() => null);
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you set no caching and use .invalidate", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.never();

      cache.invalidate(() => null);
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you set no caching and use .never", () => {
    const fn = makeStrongCacheSync((arg, cache) => {
      cache.never();

      cache.using(() => null);
    });

    expect(() => fn()).toThrow(/Caching has already been configured/);
  });

  it("should throw if you configure .forever after exiting", () => {
    const fn = makeStrongCacheSync((arg, cache) => cache);

    expect(() => fn().forever()).toThrow(
      /Cannot change caching after evaluation/,
    );
  });

  it("should throw if you configure .never after exiting", () => {
    const fn = makeStrongCacheSync((arg, cache) => cache);

    expect(() => fn().never()).toThrow(
      /Cannot change caching after evaluation/,
    );
  });

  it("should throw if you configure .using after exiting", () => {
    const fn = makeStrongCacheSync((arg, cache) => cache);

    expect(() => fn().using(() => null)).toThrow(
      /Cannot change caching after evaluation/,
    );
  });

  it("should throw if you configure .invalidate after exiting", () => {
    const fn = makeStrongCacheSync((arg, cache) => cache);

    expect(() => fn().invalidate(() => null)).toThrow(
      /Cannot change caching after evaluation/,
    );
  });

  describe("simple", () => {
    it("should allow permacaching with cache(true)", () => {
      let count = 0;

      const fn = makeStrongCacheSync((arg, cache) => {
        cache = cache.simple();

        cache(true);
        return { arg, count: count++ };
      });

      expect(fn("one")).toEqual({ arg: "one", count: 0 });
      expect(fn("one")).toBe(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", count: 1 });
      expect(fn("two")).toBe(fn("two"));

      expect(fn("one")).not.toEqual(fn("two"));
    });

    it("should allow disabling caching with cache(false)", () => {
      let count = 0;

      const fn = makeStrongCacheSync((arg, cache) => {
        cache = cache.simple();

        cache(false);
        return { arg, count: count++ };
      });

      expect(fn("one")).toEqual({ arg: "one", count: 0 });
      expect(fn("one")).toEqual({ arg: "one", count: 1 });
      expect(fn("one")).not.toEqual(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", count: 4 });
      expect(fn("two")).toEqual({ arg: "two", count: 5 });
      expect(fn("two")).not.toEqual(fn("two"));

      expect(fn("one")).not.toEqual(fn("two"));
    });

    it("should allow caching based on a value with cache(fn)", () => {
      let count = 0;
      let other = "default";

      const fn = makeStrongCacheSync((arg, cache) => {
        cache = cache.simple();

        const val = cache(() => other);

        return { arg, val, count: count++ };
      });

      expect(fn("one")).toEqual({ arg: "one", val: "default", count: 0 });
      expect(fn("one")).toBe(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", val: "default", count: 1 });
      expect(fn("two")).toBe(fn("two"));

      other = "new";

      expect(fn("one")).toEqual({ arg: "one", val: "new", count: 2 });
      expect(fn("one")).toBe(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", val: "new", count: 3 });
      expect(fn("two")).toBe(fn("two"));

      other = "default";

      expect(fn("one")).toEqual({ arg: "one", val: "default", count: 0 });
      expect(fn("one")).toBe(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", val: "default", count: 1 });
      expect(fn("two")).toBe(fn("two"));

      other = "new";

      expect(fn("one")).toEqual({ arg: "one", val: "new", count: 2 });
      expect(fn("one")).toBe(fn("one"));

      expect(fn("two")).toEqual({ arg: "two", val: "new", count: 3 });
      expect(fn("two")).toBe(fn("two"));
    });
  });

  describe("async", () => {
    const wait = gensync({
      sync: () => {},
      errback: (t, cb) => setTimeout(cb, t),
    });

    it("should throw if the cache is configured asynchronously", async () => {
      const fn = gensync(
        makeStrongCache(function* (arg, cache) {
          yield* wait(1000);
          cache.never();
          return { arg };
        }),
      ).async;

      await expect(fn("bar")).rejects.toThrowErrorMatchingInlineSnapshot(
        `"Cannot change caching after evaluation has completed."`,
      );
    });

    it("should allow asynchronous cache invalidation functions", async () => {
      const fn = gensync(
        makeStrongCache(function* (arg, cache) {
          yield* waitFor(
            cache.using(async () => {
              await wait.async(50);
              return "x";
            }),
          );
          return { arg };
        }),
      ).async;

      const [res1, res2] = await Promise.all([fn("foo"), fn("foo")]);

      expect(res1).toBe(res2);
    });

    it("should allow synchronous yield before cache configuration", async () => {
      const fn = gensync(
        makeStrongCache(function* (arg, cache) {
          yield* gensync({
            sync: () => 2,
            errback: cb => cb(null, 2),
          })();
          cache.forever();
          return { arg };
        }),
      ).async;

      const [res1, res2] = await Promise.all([fn("foo"), fn("foo")]);

      expect(res1).toBe(res2);
    });
  });
});
