import { NavDirections } from "../../../hooks/useFullKeyboardListeners";
import {
  getOppositeDirection,
  getDirectionMaps,
  getOutmostElementInDirection,
  getNextElementToFocusInDirection
} from "../helper";

describe("GridKeyboardNavigationContext.helper", () => {
  const ELEMENT1 = { current: "e1" };
  const ELEMENT2 = { current: "e2" };
  const ELEMENT3 = { current: "e3" };
  const ELEMENT4 = { current: "e4" };
  const ELEMENT5 = { current: "e5" };
  const UNMOUNTED_ELEMENT_1 = { current: null };
  const UNMOUNTED_ELEMENT_2 = { current: null };
  const DISABLED_ELEMENT = { current: { disabled: true } };
  const DISABLED_DATASET_ELEMENT = { current: { dataset: { disabled: "true" } } };
  const DATASET_NOT_DISABLED_ELEMENT = { current: { dataset: { disabled: "false" } } };

  describe("getDirectionMaps", () => {
    it("should return empty direction maps when no positions are supplied", () => {
      const input = [];
      const expected = {
        [NavDirections.RIGHT]: new Map(),
        [NavDirections.LEFT]: new Map(),
        [NavDirections.UP]: new Map(),
        [NavDirections.DOWN]: new Map()
      };

      const result = getDirectionMaps(input);

      expect(result).toEqual(expected);
    });

    it("should ignore positions in unexpected object schema", () => {
      const input = [
        { topElement: ELEMENT1, bottomElement: ELEMENT2 }, // this is the only valid input
        { unexpectedKey1: ELEMENT2, unexpectedKey2: ELEMENT3 },
        { topElement: ELEMENT4 },
        { bottomElement: ELEMENT3 },
        {}
      ];
      const expected = {
        [NavDirections.RIGHT]: new Map(),
        [NavDirections.LEFT]: new Map(),
        [NavDirections.UP]: new Map([[ELEMENT2, ELEMENT1]]),
        [NavDirections.DOWN]: new Map([[ELEMENT1, ELEMENT2]])
      };

      const result = getDirectionMaps(input);

      expect(result).toEqual(expected);
    });

    it("should return directions map which represent the relation between all positions", () => {
      const input = [
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 },
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ];
      const expected = {
        [NavDirections.RIGHT]: new Map([
          [ELEMENT2, ELEMENT4], // ELEMENT4 is to the right of ELEMENT2...
          [ELEMENT4, ELEMENT5]
        ]),
        [NavDirections.LEFT]: new Map([
          [ELEMENT5, ELEMENT4],
          [ELEMENT4, ELEMENT2]
        ]),
        [NavDirections.UP]: new Map([
          [ELEMENT2, ELEMENT1],
          [ELEMENT3, ELEMENT2]
        ]),
        [NavDirections.DOWN]: new Map([
          [ELEMENT1, ELEMENT2],
          [ELEMENT2, ELEMENT3]
        ])
      };

      const result = getDirectionMaps(input);

      expect(result).toEqual(expected);
    });

    it("should throw when a circular vertical positioning is added", () => {
      const input = [
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT1 }, // invalid
        { leftElement: ELEMENT2, rightElement: ELEMENT4 }, // valid
        { leftElement: ELEMENT4, rightElement: ELEMENT5 } // valid
      ];

      expect(() => getDirectionMaps(input)).toThrowError(
        "Circular positioning detected: the BOTTOM element is already positioned to the TOP of the TOP element. This probably means the layout isn't ordered correctly."
      );
    });

    it("should throw when a circular horizontal positioning is added", () => {
      const input = [
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT2 }, // invalid
        { topElement: ELEMENT1, bottomElement: ELEMENT2 }, // valid
        { topElement: ELEMENT2, bottomElement: ELEMENT5 } // valid
      ];

      expect(() => getDirectionMaps(input)).toThrowError(
        "Circular positioning detected: the RIGHT element is already positioned to the LEFT of the LEFT element. This probably means the layout isn't ordered correctly."
      );
    });
  });

  describe("getOppositeDirection", () => {
    [
      { direction: NavDirections.LEFT, opposite: NavDirections.RIGHT },
      { direction: NavDirections.RIGHT, opposite: NavDirections.LEFT },
      { direction: NavDirections.UP, opposite: NavDirections.DOWN },
      { direction: NavDirections.DOWN, opposite: NavDirections.UP }
    ].forEach(({ direction, opposite }) => {
      it(`should return "${opposite}" as the opposite of "${direction}"`, () => {
        expect(getOppositeDirection(direction)).toBe(opposite);
      });
    });

    it("should throw on unknown input", () => {
      const input = "UNKNOWN!";
      expect(() => getOppositeDirection(input)).toThrowError(`Unexpected direction: ${input}`);
    });
  });

  describe("getOutmostElementToFocus", () => {
    it("should return the right-most element when there are multiple horizontal connections", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 },
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.RIGHT;
      const expected = ELEMENT5;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should return the left-most element when there are multiple horizontal connections", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 },
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.LEFT;
      const expected = ELEMENT2;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should return the top-most element when there are multiple horizontal connections", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 },
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.UP;
      const expected = ELEMENT1;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should return the bottom-most element when there are multiple horizontal connections", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 },
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.DOWN;
      const expected = ELEMENT3;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should fallback to the left-most element when asking for the BOTTOM element, and there are no vertical relations", () => {
      const directionMaps = getDirectionMaps([
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.DOWN;
      const expected = ELEMENT2;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should fallback to the left-most element when asking for the TOP element, and there are no vertical relations", () => {
      const directionMaps = getDirectionMaps([
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const direction = NavDirections.UP;
      const expected = ELEMENT2;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should fallback to the top-most element when asking for the LEFT element, and there are no horizontal relations", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 }
      ]);
      const direction = NavDirections.LEFT;
      const expected = ELEMENT1;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should fallback to the top-most element when asking for the RIGHT element, and there are no horizontal relations", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: ELEMENT3 }
      ]);
      const direction = NavDirections.RIGHT;
      const expected = ELEMENT1;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should skip the bottom-most element when asking for the bottom element, and the bottom-most element is not mounted", () => {
      const directionMaps = getDirectionMaps([
        { topElement: ELEMENT1, bottomElement: ELEMENT2 },
        { topElement: ELEMENT2, bottomElement: UNMOUNTED_ELEMENT_1 }
      ]);
      const direction = NavDirections.DOWN;
      const expected = ELEMENT2;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });

    it("should skip the two right-most elements when asking for the right element, and the two right-most elements are not mounted", () => {
      const directionMaps = getDirectionMaps([
        { leftElement: ELEMENT1, rightElement: UNMOUNTED_ELEMENT_2 },
        { leftElement: UNMOUNTED_ELEMENT_2, rightElement: UNMOUNTED_ELEMENT_1 }
      ]);
      const direction = NavDirections.RIGHT;
      const expected = ELEMENT1;

      const result = getOutmostElementInDirection(directionMaps, direction);

      expect(result).toEqual(expected);
    });
  });

  describe("getNextElementToFocusInDirection", () => {
    it("should return null if the referenced element isn't positioned on that direction", () => {
      const directionMaps = getDirectionMaps([
        { leftElement: ELEMENT2, rightElement: ELEMENT4 },
        { leftElement: ELEMENT4, rightElement: ELEMENT5 }
      ]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1); // ELEMENT1 isn't mapped

      expect(result).toBeNull();
    });

    it("return null if there's only one next ref, and it is currently null", () => {
      const directionMaps = getDirectionMaps([{ leftElement: ELEMENT1, rightElement: UNMOUNTED_ELEMENT_1 }]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1);

      expect(result).toBeNull();
    });

    it("return null if there's only one next ref, and it is disabled", () => {
      const directionMaps = getDirectionMaps([{ leftElement: ELEMENT1, rightElement: DISABLED_ELEMENT }]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1);

      expect(result).toBeNull();
    });

    it("return null if there's only one next ref, and it is disabled with data-disabled='true'", () => {
      const directionMaps = getDirectionMaps([{ leftElement: ELEMENT1, rightElement: DISABLED_DATASET_ELEMENT }]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1);

      expect(result).toBeNull();
    });

    it("return the next element ref even if it has data-disabled='false'", () => {
      const directionMaps = getDirectionMaps([{ leftElement: ELEMENT1, rightElement: DATASET_NOT_DISABLED_ELEMENT }]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1);

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

    it("return the next element ref in the given direction which is focusable, while skipping disabled or unmounted elements", () => {
      const directionMaps = getDirectionMaps([
        { leftElement: ELEMENT1, rightElement: UNMOUNTED_ELEMENT_1 },
        { leftElement: UNMOUNTED_ELEMENT_1, rightElement: DISABLED_ELEMENT },
        { leftElement: DISABLED_ELEMENT, rightElement: ELEMENT2 }
      ]);
      const directionMap = directionMaps[NavDirections.RIGHT];

      const result = getNextElementToFocusInDirection(directionMap, ELEMENT1);

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