import { localStorage as NativeLocalStorage } from "../__mocks__/ios";
import { DEFAULT_NAMESPACE } from "../__mocks__/storageMock";

const { localStorage } = require("../LocalStorage");

const RN = require("react-native");

RN.NativeModules.LocalStorage = NativeLocalStorage;

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() {
  NativeLocalStorage.setItem(test_key, test_value);

  NativeLocalStorage.setItem(namespaced_key, namespaced_value, test_namespace);

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

describe("localStorage", () => {
  describe("setItem", () => {
    beforeEach(async () => {
      NativeLocalStorage.__clear();
    });

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

      expect(result).toBe(true);

      expect(NativeLocalStorage.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 = localStorage.setItem;
      const result = await setItem(key, value);

      expect(result).toBe(true);

      expect(NativeLocalStorage.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 localStorage.setItem(key, value);

      expect(result).toBe(true);

      expect(NativeLocalStorage.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 localStorage.setItem(key, value, namespace);

      expect(result).toBe(true);

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

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

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

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

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

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

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

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

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

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

      expect(() =>
        localStorage.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 = localStorage.addListener(
            { key: namespaced_key, namespace: test_namespace },
            listener
          );
        });

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

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

        it("invokes the listener when the key is changed and is invoked as a bound method", async () => {
          const setItem = localStorage.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 localStorage.setItem(test_key, new_value);

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

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

          await localStorage.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 = localStorage.addListener(
            { namespace: test_namespace },
            listener
          );
        });

        it("is invoked when a key in the namespace is changed", async () => {
          await localStorage.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 localStorage.setItem(test_key, new_value, DEFAULT_NAMESPACE);

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

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

          await localStorage.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 = localStorage.addListener({}, listener);
        });

        it("is invoked when a key is changed in the default namespace", async () => {
          await localStorage.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 localStorage.setItem(namespaced_key, new_value, test_namespace);

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

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

          await localStorage.setItem(test_key, new_value);

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

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

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

    it("retrieves a value in a namespace", async () => {
      const result = await localStorage.getItem(namespaced_key, test_namespace);
      expect(result).toBe(namespaced_value);
    });

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

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

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

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

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

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

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

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

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

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

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

      expect(result).toBe(true);

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

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

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

    it("throws if a native error is thrown", async () => {
      expect(() =>
        localStorage.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 = localStorage.addListener(
            { key: namespaced_key, namespace: test_namespace },
            listener
          );
        });

        it("invokes the listener when this key is changed", async () => {
          await localStorage.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 localStorage.removeItem(test_key);

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

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

          await localStorage.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 = localStorage.addListener(
            { namespace: test_namespace },
            listener
          );
        });

        it("is invoked when a key in the namespace is changed", async () => {
          await localStorage.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 localStorage.removeItem(test_key, DEFAULT_NAMESPACE);

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

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

          await localStorage.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 = localStorage.addListener({}, listener);
        });

        it("is invoked when a key is changed in the default namespace", async () => {
          await localStorage.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 localStorage.removeItem(namespaced_key, test_namespace);

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

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

          await localStorage.removeItem(test_key);

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

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

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

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

    it("works as a bound method", async () => {
      const getAllItems = localStorage.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 localStorage.getAllItems(test_namespace);

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

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