import { inspect } from "util";
import { AsyncResult, Err, Ok, Result, resultify } from "../";
import { tryAssertErr, tryAssertOk } from ".";

function ntoa(n: number): string {
  return `${n}`;
}

function tryNtoA(n: number): Result<string, []> {
  return Ok(ntoa(n));
}

async function tryNtoAAsync(n: number): AsyncResult<string, []> {
  return tryNtoA(n);
}

function tryAtoN(a: string): Result<number, []> {
  try {
    const n = Number(a);
    return Number.isNaN(n) ? Err([]) : Ok(n);
  } catch (e) {
    return Err([]);
  }
}

async function tryAtoNAsync(a: string): AsyncResult<number, []> {
  return tryAtoN(a);
}

describe("@/api/common/result", () => {
  describe("Result", () => {
    it("isOk narrows in short circuit", () => {
      const r = tryNtoA(1).andThen(tryAtoN);

      if (r.isOk()) {
        const t: number = r.value;
        expect(t).toEqual(1);
        return;
      }
      // never executes but does compile
      expect(r.error).toEqual([]);
    });

    it("isErr narrows field in short circuit", () => {
      const r = tryAtoN("x");

      if (r.isErr()) {
        const e: [] = r.error;
        expect(e).toEqual([]);
        return;
      }
      // will never execute, demonstrates that tsc still narrows
      expect(r.value).toEqual(1);
    });

    it("flat maps a success", () => {
      const r = tryNtoA(1);
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
      tryAssertOk(r, (t) => {
        expect(t).toEqual("1");
      });
      const r2 = r.andThen(tryAtoN);
      expect(r2.isOk()).toBeTruthy();
      expect(r2.isErr()).toBeFalsy();
      tryAssertOk(r2, (t) => {
        expect(t).toEqual(1);
      });
    });

    it("async flat maps a success", async () => {
      const r = await tryNtoAAsync(1);
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
      tryAssertOk(r, (t) => {
        expect(t).toEqual("1");
      });
      const r2 = await resultify(tryNtoAAsync(1)).andThenAsync(tryAtoNAsync);
      expect(r2.isOk()).toBeTruthy();
      expect(r2.isErr()).toBeFalsy();
      tryAssertOk(r2, (t) => {
        expect(t).toEqual(1);
      });
    });

    it("flat maps an error", () => {
      const r = tryAtoN("x");
      expect(r.isErr()).toBeTruthy();
      expect(r.isOk()).toBeFalsy();
      const e = r.andThen(tryNtoA);
      expect(e.isErr()).toBeTruthy();
      expect(e.isOk()).toBeFalsy();
    });

    it("async flat maps an error", async () => {
      const r = tryAtoN("x");
      expect(r.isErr()).toBeTruthy();
      expect(r.isOk()).toBeFalsy();
      const e = await r.andThenAsync(tryNtoAAsync);
      expect(e.isErr()).toBeTruthy();
      expect(e.isOk()).toBeFalsy();
    });

    it("maps a success", () => {
      const r = tryAtoN("1").yield((n) => n + 1);
      tryAssertOk(r, (t) => {
        expect(t).toEqual(2);
      });
    });

    it("async maps a success", async () => {
      const r = await tryAtoN("1").yieldAsync(async (n) => n + 1);
      tryAssertOk(r, (t) => {
        expect(t).toEqual(2);
      });
    });

    it("maps an error", () => {
      const e = tryAtoN("x").yield((n) => n + 1);
      expect(e.isErr()).toBeTruthy();
      expect(e.isOk()).toBeFalsy();
    });

    it("maps an error type on success", () => {
      const r = tryAtoN("1").yieldErr((e) => {
        inspect(e);
        return "failed";
      });
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
    });

    it("maps an error type on error", () => {
      const e = tryAtoN("x").yieldErr((e) => {
        inspect(e);
        return "failed";
      });
      expect(e.isOk()).toBeFalsy();
      expect(e.isErr()).toBeTruthy();
      tryAssertErr(e, (e) => {
        expect(e).toEqual("failed");
      });
    });

    it("and handles success", () => {
      const r: Result<string, []> = tryAtoN("1").and(Ok("success"));
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
      tryAssertOk(r, (t) => {
        expect(t).toEqual("success");
      });
    });

    it("and handles error", () => {
      const r: Result<string, []> = tryAtoN("x").and(Ok("success"));
      expect(r.isOk()).toBeFalsy();
      expect(r.isErr()).toBeTruthy();
    });

    it("or handles success", () => {
      const r: Result<number, string> = tryAtoN("1").or(Ok(0));
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
      tryAssertOk(r, (t) => {
        expect(t).toEqual(1);
      });
    });

    it("or handles error", () => {
      const r: Result<number, string> = tryAtoN("x").or(Ok(0));
      expect(r.isOk()).toBeTruthy();
      expect(r.isErr()).toBeFalsy();
      tryAssertOk(r, (t) => {
        expect(t).toEqual(0);
      });
    });
  });
});
