import { sessionStorage as NativeSessionStorage } from "../__mocks__/ios";
import { DEFAULT_NAMESPACE } from "../__mocks__/storageMock";
import { NativeModules } from "react-native";

NativeModules.SessionStorage = NativeSessionStorage;

const { sessionStorage } = require("../SessionStorage");

const test_key = "test_key";
const test_value = "test_value";
const namespaced_key = "namespaced_key";
const namespaced_value = "namespaced_value";
const test_namespace = "test_namespace";
const obj_key = "obj_key";
const obj_value = { prop: "value" };

function setUpStorage() {
  NativeSessionStorage.setItem(test_key, test_value);

  NativeSessionStorage.setItem(
    namespaced_key,
    namespaced_value,
    test_namespace
  );

  NativeSessionStorage.setItem(obj_key, JSON.stringify(obj_value));
}

describe("sessionStorage", () => {
  describe("setItem", () => {
    beforeEach(async () => {
      NativeSessionStorage.__clear();
    });

    it("sets an item in storage", async () => {
      const key = "test_key";
      const value = "test_value";
      const result = await sessionStorage.setItem(key, value);

      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(DEFAULT_NAMESPACE).get(key)
      ).toEqual(value);
    });

    it("works as a bound method", async () => {
      const key = "test_key";
      const value = "test_value";
      const setItem = sessionStorage.setItem;
      const result = await setItem(key, value);

      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(DEFAULT_NAMESPACE).get(key)
      ).toEqual(value);
    });

    it("stringifies the value if its not a string", async () => {
      const key = "test_key";
      const value = { objVal: "test_value" };

      const result = await sessionStorage.setItem(key, value);

      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(DEFAULT_NAMESPACE).get(key)
      ).toEqual(JSON.stringify(value));
    });

    it("sets a value in a specific namespace", async () => {
      const key = "test_key";
      const value = "test_value";
      const namespace = "test_namespace";
      const result = await sessionStorage.setItem(key, value, namespace);

      expect(result).toBe(true);

      expect(NativeSessionStorage.items.get(namespace).get(key)).toEqual(value);
    });

    it("throws if key is null", async () => {
      const value = "test_value";

      expect(() =>
        sessionStorage.setItem(null, value)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if key is undefined", async () => {
      const value = "test_value";

      expect(() =>
        sessionStorage.setItem(undefined, value)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if value is null", async () => {
      const key = "test_key";

      expect(() =>
        sessionStorage.setItem(key, null)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if value is undefined", async () => {
      const key = "test_key";

      expect(() =>
        sessionStorage.setItem(key, undefined)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if there is an error in the native module", async () => {
      const key = "fail";
      const value = "value";

      expect(() =>
        sessionStorage.setItem(key, value)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    describe("listeners", () => {
      describe("listening for a specific key and namespace change", () => {
        const listener = jest.fn();
        const new_value = "new_value";
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener(
            { key: namespaced_key, namespace: test_namespace },
            listener
          );
        });

        it("invokes the listener when this key is changed", async () => {
          await sessionStorage.setItem(
            namespaced_key,
            new_value,
            test_namespace
          );

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: namespaced_key,
              namespace: test_namespace,
              value: new_value,
            })
          );
        });

        it("works as a bound method", async () => {
          const setItem = sessionStorage.setItem;
          await setItem(namespaced_key, new_value, test_namespace);

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: namespaced_key,
              namespace: test_namespace,
              value: new_value,
            })
          );
        });

        it("is not invoked when another key is changed", async () => {
          await sessionStorage.setItem(test_key, new_value);

          expect(listener).not.toHaveBeenCalled();
        });

        it("removes the listener when invoking the remove function", async () => {
          removeListener();

          await sessionStorage.setItem(
            namespaced_key,
            new_value,
            test_namespace
          );

          expect(listener).not.toHaveBeenCalled();
        });
      });

      describe("listening for any key change in a namespace", () => {
        const listener = jest.fn();
        const new_value = "new_value";
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener(
            { namespace: test_namespace },
            listener
          );
        });

        it("is invoked when a key in the namespace is changed", async () => {
          await sessionStorage.setItem(
            namespaced_key,
            new_value,
            test_namespace
          );

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: namespaced_key,
              value: new_value,
              namespace: test_namespace,
            })
          );
        });

        it("is not invoked when a key in a different namespace is changed", async () => {
          await sessionStorage.setItem(test_key, new_value, DEFAULT_NAMESPACE);

          expect(listener).not.toHaveBeenCalled();
        });

        it("is not invoked after being removed", async () => {
          removeListener();

          await sessionStorage.setItem(
            namespaced_key,
            new_value,
            test_namespace
          );

          expect(listener).not.toHaveBeenCalled();
        });
      });

      describe("listening for key changes in the default namespace", () => {
        const listener = jest.fn();
        const new_value = "new_value";
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener({}, listener);
        });

        it("is invoked when a key is changed in the default namespace", async () => {
          await sessionStorage.setItem(test_key, new_value);

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: test_key,
              value: new_value,
              namespace: DEFAULT_NAMESPACE,
            })
          );
        });

        it("is not invoked when a key in a nother namespace is changed", async () => {
          await sessionStorage.setItem(
            namespaced_key,
            new_value,
            test_namespace
          );

          expect(listener).not.toHaveBeenCalled();
        });

        it("is not invoked after having been removed", async () => {
          removeListener();

          await sessionStorage.setItem(test_key, new_value);

          expect(listener).not.toHaveBeenCalled();
        });
      });
    });
  });

  describe("getItem", () => {
    beforeAll(setUpStorage);

    it("retrieves a value from storage", async () => {
      const result = await sessionStorage.getItem(test_key);
      expect(result).toBe(test_value);
    });

    it("works as a bound method", async () => {
      const getItem = sessionStorage.getItem;
      const result = await getItem(test_key);
      expect(result).toBe(test_value);
    });

    it("retrieves a value in a namespace", async () => {
      const result = await sessionStorage.getItem(
        namespaced_key,
        test_namespace
      );

      expect(result).toBe(namespaced_value);
    });

    it("retrieves non string values", async () => {
      const result = await sessionStorage.getItem(obj_key);
      expect(result).toEqual(obj_value);
    });

    it("throws if key is null", async () => {
      expect(() =>
        sessionStorage.getItem(null)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if key is undefined", async () => {
      expect(() =>
        sessionStorage.getItem()
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if there is an error on the native side", async () => {
      expect(() =>
        sessionStorage.getItem("fail")
      ).rejects.toThrowErrorMatchingSnapshot();
    });
  });

  describe("removeItem", () => {
    beforeEach(setUpStorage);

    it("removes a value from storage", async () => {
      const result = await sessionStorage.removeItem(test_key);
      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(DEFAULT_NAMESPACE).get(test_key)
      ).toBeUndefined();
    });

    it("works as a bound method", async () => {
      const removeItem = sessionStorage.removeItem;
      const result = await removeItem(test_key);
      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(DEFAULT_NAMESPACE).get(test_key)
      ).toBeUndefined();
    });

    it("removes a value in a namespace", async () => {
      const result = await sessionStorage.removeItem(
        namespaced_key,
        test_namespace
      );

      expect(result).toBe(true);

      expect(
        NativeSessionStorage.items.get(test_namespace).get(namespaced_key)
      ).toBeUndefined();
    });

    it("throws if key is null", async () => {
      expect(() =>
        sessionStorage.removeItem(null)
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if key is undefined", async () => {
      expect(() =>
        sessionStorage.removeItem()
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    it("throws if a native error is thrown", async () => {
      expect(() =>
        sessionStorage.removeItem("fail")
      ).rejects.toThrowErrorMatchingSnapshot();
    });

    describe("listeners", () => {
      describe("listening for a specific key and namespace change", () => {
        const listener = jest.fn();
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener(
            { key: namespaced_key, namespace: test_namespace },
            listener
          );
        });

        it("invokes the listener when this key is changed", async () => {
          await sessionStorage.removeItem(namespaced_key, test_namespace);

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: namespaced_key,
              namespace: test_namespace,
              value: null,
            })
          );
        });

        it("is not invoked when another key is changed", async () => {
          await sessionStorage.removeItem(test_key);

          expect(listener).not.toHaveBeenCalled();
        });

        it("removes the listener when invoking the remove function", async () => {
          removeListener();

          await sessionStorage.removeItem(namespaced_key, test_namespace);

          expect(listener).not.toHaveBeenCalled();
        });
      });

      describe("listening for any key change in a namespace", () => {
        const listener = jest.fn();
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener(
            { namespace: test_namespace },
            listener
          );
        });

        it("is invoked when a key in the namespace is changed", async () => {
          await sessionStorage.removeItem(namespaced_key, test_namespace);

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: namespaced_key,
              value: null,
              namespace: test_namespace,
            })
          );
        });

        it("is not invoked when a key in a different namespace is changed", async () => {
          await sessionStorage.removeItem(test_key, DEFAULT_NAMESPACE);

          expect(listener).not.toHaveBeenCalled();
        });

        it("is not invoked after being removed", async () => {
          removeListener();

          await sessionStorage.removeItem(namespaced_key, test_namespace);

          expect(listener).not.toHaveBeenCalled();
        });
      });

      describe("listening for key changes in the default namespace", () => {
        const listener = jest.fn();
        let removeListener;

        beforeEach(() => {
          setUpStorage();
          listener.mockClear();

          removeListener = sessionStorage.addListener({}, listener);
        });

        it("is invoked when a key is changed in the default namespace", async () => {
          await sessionStorage.removeItem(test_key);

          expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({
              key: test_key,
              value: null,
              namespace: DEFAULT_NAMESPACE,
            })
          );
        });

        it("is not invoked when a key in a nother namespace is changed", async () => {
          await sessionStorage.removeItem(namespaced_key, test_namespace);

          expect(listener).not.toHaveBeenCalled();
        });

        it("is not invoked after having been removed", async () => {
          removeListener();

          await sessionStorage.removeItem(test_key);

          expect(listener).not.toHaveBeenCalled();
        });
      });
    });
  });

  describe("getAllItems", () => {
    beforeAll(setUpStorage);

    it("retrieves all values", async () => {
      const result = await sessionStorage.getAllItems();

      expect(result).toEqual(
        expect.objectContaining({
          [test_key]: test_value,
          [obj_key]: obj_value,
        })
      );
    });

    it("works as a bound method", async () => {
      const getAllItems = sessionStorage.getAllItems;
      const result = await getAllItems();

      expect(result).toEqual(
        expect.objectContaining({
          [test_key]: test_value,
          [obj_key]: obj_value,
        })
      );
    });

    it("retrieves all values in a namespace", async () => {
      const result = await sessionStorage.getAllItems(test_namespace);

      expect(result).toEqual(
        expect.objectContaining({ [namespaced_key]: namespaced_value })
      );
    });

    it("throws if an error happen on native side", async () => {
      expect(() =>
        sessionStorage.getAllItems("undefined_namespace")
      ).rejects.toThrowErrorMatchingSnapshot();
    });
  });
});
