import { Observer } from "../../src/core/observer";
import { nextMicroTick } from "../helpers";

describe("observer", () => {
  test("properly observe objects", () => {
    const observer = new Observer();
    const obj: any = observer.observe({});

    expect(typeof obj).toBe("object");
    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);

    const obj2: any = observer.observe({ a: 1 });
    expect(observer.revNumber(obj2)).toBe(1);
    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);

    obj2.a = 2;

    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.revNumber(obj2)).toBe(2);
    expect(observer.rev).toBe(2);

    expect(obj2).toEqual({
      a: 2
    });
  });

  test("properly handle null or undefined", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ a: null, b: undefined });

    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);

    obj.a = 3;
    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);

    obj.b = 5;
    expect(observer.revNumber(obj)).toBe(3);
    expect(observer.deepRevNumber(obj)).toBe(3);
    expect(observer.rev).toBe(3);

    obj.a = null;
    obj.b = undefined;
    expect(observer.revNumber(obj)).toBe(5);
    expect(observer.deepRevNumber(obj)).toBe(5);
    expect(observer.rev).toBe(5);
    expect(obj).toEqual({
      a: null,
      b: undefined
    });
  });

  test("properly handle dates", () => {
    const observer = new Observer();
    const date = new Date();
    const obj: any = observer.observe({ date });

    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);
    expect(typeof obj.date.getFullYear()).toBe('number');
    expect(obj.date).toBe(date);

    obj.date = new Date();

    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);
    expect(obj.date).not.toBe(date);
  });

  test("can change values in array", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ arr: [1, 2] });

    expect(Array.isArray(obj.arr)).toBe(true);
    expect(observer.revNumber(obj.arr)).toBe(1);
    expect(observer.deepRevNumber(obj.arr)).toBe(1);
    expect(observer.rev).toBe(1);

    obj.arr[0] = "nope";
    expect(observer.revNumber(obj.arr)).toBe(2);
    expect(observer.deepRevNumber(obj.arr)).toBe(2);
    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);
  });

  test("various object property changes", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ a: 1 });

    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);

    obj.a = 2;

    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);

    // same value again
    obj.a = 2;
    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);

    obj.a = 3;
    expect(observer.revNumber(obj)).toBe(3);
    expect(observer.deepRevNumber(obj)).toBe(3);
    expect(observer.rev).toBe(3);
  });

  test("properly observe arrays", () => {
    const observer = new Observer();
    const arr: any = observer.observe([]);

    expect(Array.isArray(arr)).toBe(true);
    expect(arr.length).toBe(0);
    expect(observer.revNumber(arr)).toBe(1);
    expect(observer.deepRevNumber(arr)).toBe(1);
    expect(observer.rev).toBe(1);

    arr.push(1);
    expect(observer.revNumber(arr)).toBe(2);
    expect(observer.deepRevNumber(arr)).toBe(2);
    expect(observer.rev).toBe(2);
    expect(arr.length).toBe(1);
    expect(arr).toEqual([1]);

    arr.splice(1, 0, "hey");
    expect(observer.revNumber(arr)).toBe(3);
    expect(observer.deepRevNumber(arr)).toBe(3);
    expect(observer.rev).toBe(3);
    expect(arr).toEqual([1, "hey"]);
    expect(arr.length).toBe(2);

    arr.unshift("lindemans");
    //it generates 3 primitive operations
    expect(observer.revNumber(arr)).toBe(6);
    expect(observer.deepRevNumber(arr)).toBe(6);
    expect(observer.rev).toBe(6);
    expect(arr).toEqual(["lindemans", 1, "hey"]);
    expect(arr.length).toBe(3);

    arr.reverse();
    //it generates 2 primitive operations
    expect(observer.revNumber(arr)).toBe(8);
    expect(observer.deepRevNumber(arr)).toBe(8);
    expect(observer.rev).toBe(8);
    expect(arr).toEqual(["hey", 1, "lindemans"]);
    expect(arr.length).toBe(3);

    arr.pop(); // one set, one delete
    expect(observer.revNumber(arr)).toBe(10);
    expect(observer.deepRevNumber(arr)).toBe(10);
    expect(observer.rev).toBe(10);
    expect(arr).toEqual(["hey", 1]);
    expect(arr.length).toBe(2);

    arr.shift(); // 2 sets, 1 delete
    expect(observer.revNumber(arr)).toBe(13);
    expect(observer.deepRevNumber(arr)).toBe(13);
    expect(observer.rev).toBe(13);
    expect(arr).toEqual([1]);
    expect(arr.length).toBe(1);
  });

  test("object pushed into arrays are observed", () => {
    const observer = new Observer();
    const arr: any = observer.observe([]);
    expect(observer.rev).toBe(1);

    arr.push({ kriek: 5 });
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(arr)).toBe(2);
    expect(observer.revNumber(arr[0])).toBe(2);

    arr[0].kriek = 6;

    expect(observer.rev).toBe(3);
    expect(observer.revNumber(arr)).toBe(2);
    expect(observer.deepRevNumber(arr)).toBe(3);
    expect(observer.revNumber(arr[0])).toBe(3);
  });

  test("set new property on observed object", async () => {
    const observer = new Observer();
    observer.notifyCB = jest.fn();
    const state: any = observer.observe({ a: 1 });
    expect(observer.notifyCB).toBeCalledTimes(0);
    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state)).toBe(1);

    state.b = 8;

    await nextMicroTick();

    expect(observer.notifyCB).toBeCalledTimes(1);
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state)).toBe(2);

    expect(state.b).toBe(8);
  });

  test("delete property from observed object", async () => {
    const observer = new Observer();
    observer.notifyCB = jest.fn();
    const state: any = observer.observe({ a: 1, b: 8 });
    observer.observe(state);

    expect(observer.notifyCB).toBeCalledTimes(0);
    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state)).toBe(1);

    delete state.b;
    await nextMicroTick();

    expect(observer.notifyCB).toBeCalledTimes(1);
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state)).toBe(2);

    expect(state).toEqual({ a: 1 });
  });

  test("set element in observed array", async () => {
    const observer = new Observer();
    observer.notifyCB = jest.fn();
    const state: any = observer.observe(["a"]);

    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.deepRevNumber(state)).toBe(1);
    expect(observer.notifyCB).toBeCalledTimes(0);

    state[1] = "b";

    await nextMicroTick();

    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state)).toBe(2);
    expect(observer.deepRevNumber(state)).toBe(2);
    expect(observer.notifyCB).toBeCalledTimes(1);

    expect(state).toEqual(["a", "b"]);
  });

  test("properly observe arrays in object", () => {
    const observer = new Observer();
    const state: any = observer.observe({ arr: [] });

    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state.arr)).toBe(1);
    expect(observer.deepRevNumber(state.arr)).toBe(1);
    expect(state.arr.length).toBe(0);

    state.arr.push(1);
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state.arr)).toBe(2);
    expect(observer.deepRevNumber(state.arr)).toBe(2);
    expect(state.arr.length).toBe(1);
  });

  test("properly observe objects in array", () => {
    const observer = new Observer();
    const state: any = observer.observe({ arr: [{ something: 1 }] });
    observer.observe(state);

    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state.arr)).toBe(1);
    expect(observer.revNumber(state.arr[0])).toBe(1);

    state.arr[0].something = 2;
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state.arr)).toBe(1);
    expect(observer.revNumber(state.arr[0])).toBe(2);
  });

  test("properly observe objects in object", () => {
    const observer = new Observer();
    const state: any = observer.observe({ a: { b: 1 } });

    expect(observer.rev).toBe(1);
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.revNumber(state.a)).toBe(1);

    state.a.b = 2;
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.revNumber(state.a)).toBe(2);
  });

  test("reobserve new object values", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ a: 1 });

    expect(observer.rev).toBe(1);
    expect(observer.revNumber(obj)).toBe(1);

    obj.a = { b: 2 };

    expect(observer.rev).toBe(2);
    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.revNumber(obj.a)).toBe(2);
    obj.a.b = 3;
    expect(observer.rev).toBe(3);
    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.revNumber(obj.a)).toBe(3);
  });

  test("deep observe misc changes", () => {
    const observer = new Observer();
    const state: any = observer.observe({ o: { a: 1 }, arr: [1], n: 13 });
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.deepRevNumber(state)).toBe(1);

    state.o.a = 2;
    expect(observer.rev).toBe(2);
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.deepRevNumber(state)).toBe(2);

    state.arr.push(2);
    expect(observer.rev).toBe(3);
    expect(observer.revNumber(state)).toBe(1);
    expect(observer.deepRevNumber(state)).toBe(3);

    state.n = 155;
    expect(observer.rev).toBe(4);
    expect(observer.revNumber(state)).toBe(2);
    expect(observer.deepRevNumber(state)).toBe(4);
  });

  test("properly handle already observed state", () => {
    const observer = new Observer();
    const obj1: any = observer.observe({ a: 1 });
    const obj2: any = observer.observe({ b: 1 });

    expect(observer.revNumber(obj1)).toBe(1);
    expect(observer.revNumber(obj2)).toBe(1);

    obj1.a = 2;
    obj2.b = 3;
    expect(observer.revNumber(obj1)).toBe(2);
    expect(observer.revNumber(obj2)).toBe(2);

    obj2.b = obj1;
    expect(observer.revNumber(obj1)).toBe(2);
    expect(observer.revNumber(obj2)).toBe(3);
  });

  test("can set a property more than once", () => {
    const observer = new Observer();
    const obj: any = observer.observe({});

    expect(observer.revNumber(obj)).toBe(1);
    expect(observer.deepRevNumber(obj)).toBe(1);
    expect(observer.rev).toBe(1);

    obj.aku = "always finds annoying problems";
    expect(observer.revNumber(obj)).toBe(2);
    expect(observer.deepRevNumber(obj)).toBe(2);
    expect(observer.rev).toBe(2);

    obj.aku = "always finds good problems";

    expect(observer.revNumber(obj)).toBe(3);
    expect(observer.deepRevNumber(obj)).toBe(3);
    expect(observer.rev).toBe(3);
  });

  test("properly handle swapping elements", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ a: { arr: [] }, b: 1 });

    // swap a and b
    const b = obj.b;
    obj.b = obj.a;
    obj.a = b;
    expect(observer.rev).toBe(3);

    // push something into array to make sure it works
    obj.b.arr.push("blanche");
    expect(observer.rev).toBe(4);
  });

  test("properly handle assigning observed obj containing array", () => {
    const observer = new Observer();
    const obj: any = observer.observe({ a: { arr: [], val: "test" } });

    expect(observer.rev).toBe(1);
    obj.a = { ...obj.a, val: "test2" };
    expect(observer.rev).toBe(2);

    // push something into array to make sure it works
    obj.a.arr.push("blanche");
    expect(observer.rev).toBe(3);
  });

  test("accept cycles in observed state", () => {
    const observer = new Observer();
    let obj1: any = {};
    let obj2: any = { b: obj1, key: 1 };
    obj1.a = obj2;
    obj1 = observer.observe(obj1);
    obj2 = obj1.a;

    expect(observer.revNumber(obj1)).toBe(1);
    expect(observer.revNumber(obj2)).toBe(1);

    obj2.key = 3;
    expect(observer.revNumber(obj1)).toBe(1);
    expect(observer.revNumber(obj2)).toBe(2);
  });

  test("call callback when state is changed", async () => {
    const observer = new Observer();
    observer.notifyCB = jest.fn();
    const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });

    expect(observer.notifyCB).toBeCalledTimes(0);

    obj.a = 2;
    await nextMicroTick();
    expect(observer.notifyCB).toBeCalledTimes(1);

    obj.b.c = 3;
    await nextMicroTick();
    expect(observer.notifyCB).toBeCalledTimes(2);

    obj.d[0].e = 5;
    await nextMicroTick();
    expect(observer.notifyCB).toBeCalledTimes(3);

    obj.a = 111;
    obj.f = 222;
    await nextMicroTick();
    expect(observer.notifyCB).toBeCalledTimes(4);
  });

  test("throw error when state is mutated in object if allowMutation=false", async () => {
    const observer = new Observer();
    observer.allowMutations = false;
    const obj: any = observer.observe({ a: 1 });
    expect(() => {
      obj.a = 2;
    }).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');
  });

  test("throw error when state is mutated in array if allowMutation=false", async () => {
    const observer = new Observer();
    observer.allowMutations = false;
    const obj: any = observer.observe({ a: [1] });

    expect(() => {
      obj.a.push(2);
    }).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
  });
});
