import { up, down, toggle } from "./index";
import { screen } from "@testing-library/dom";

const addMockAnimation = (element, id = "") => {
  const mockAnimation = {
    finish: jest.fn(),
    id,
  };

  element.getAnimations = () => [mockAnimation];

  return mockAnimation;
};

const withMockAnimation = (element, duration = 0) => {
  const finish = jest.fn();
  const reverse = jest.fn();
  let timeCalled = null;

  element.getAnimations = () => [];
  element.animate = jest.fn(() => {
    timeCalled = new Date().getTime();

    return {
      finished: new Promise((resolve) => {
        setTimeout(resolve, duration);
      }),
      finish,
    };
  });

  return { element, finish, reverse, getTimeCalled: () => timeCalled };
};

const mockHeightOnce = (values) => {
  const mock = jest.spyOn(HTMLDivElement.prototype, "clientHeight", "get");

  return values.reduce((m, val) => m.mockImplementationOnce(() => val), mock);
};

const mockOffsetHeight = (height = null) => {
  jest
    .spyOn(HTMLDivElement.prototype, "offsetHeight", "get")
    .mockImplementation(() => height);
};

const mockHeight = (value) => {
  return jest
    .spyOn(HTMLDivElement.prototype, "clientHeight", "get")
    .mockImplementation(() => value);
};

beforeEach(() => {
  document.body.innerHTML = `<div data-testid="content" style="display: none;">Content!</div>`;
  mockHeight(100);

  window.requestAnimationFrame = (cb) => cb();

  // Does NOT prefer reduced motion.
  window.matchMedia = () => {
    return {
      matches: false,
    };
  };
});

it("opens element", (done) => {
  document.body.innerHTML = `<div data-testid="content" style="display: none;">Content!</div>`;
  const { element } = withMockAnimation(screen.getByTestId("content"));

  mockHeightOnce([0, 100]);

  down(element).then((opened) => {
    expect(opened).toBe(true);
    expect(element.animate).toBeCalledTimes(1);
    expect(element.style.display).toEqual("block");

    expect(element.animate).toHaveBeenCalledWith(
      [
        expect.objectContaining({
          height: "0px",
          paddingBottom: "0px",
          paddingTop: "0px",
        }),
        expect.objectContaining({
          height: "100px",
          paddingBottom: "",
          paddingTop: "",
        }),
      ],
      { easing: "ease", duration: 250, fill: "backwards" }
    );

    done();
  });
});

it("closes element", (done) => {
  document.body.innerHTML = `<div data-testid="content" style="height: 100px">Content!</div>`;
  const { element } = withMockAnimation(screen.getByTestId("content"));

  up(element).then((opened) => {
    expect(opened).toBe(false);
    expect(element.animate).toBeCalledTimes(1);
    expect(element.style.display).toEqual("none");
    expect(element.animate).toHaveBeenCalledWith(
      [
        expect.objectContaining({
          height: "100px",
          paddingBottom: "",
          paddingTop: "",
        }),
        expect.objectContaining({
          height: "0px",
          paddingBottom: "0px",
          paddingTop: "0px",
        }),
      ],
      { easing: "ease", duration: 250, fill: "backwards" }
    );

    done();
  });
});

describe("toggle()", () => {
  beforeEach(() => {
    document.body.innerHTML = `<div data-testid="content" style="display: none;">Content!</div>`;
  });

  describe("animation is allowed to complete fully", () => {
    it("toggles element open", (done) => {
      const { element } = withMockAnimation(screen.getByTestId("content"));

      toggle(element).then((opened) => {
        expect(opened).toBe(true);
        expect(element.animate).toBeCalledTimes(1);

        done();
      });
    });

    it("toggles element closed", (done) => {
      const { element } = withMockAnimation(screen.getByTestId("content"));

      // Give it an arbitrary height to mock it being "open."
      mockOffsetHeight(100);

      toggle(element).then((opened) => {
        expect(opened).toBe(false);
        expect(element.animate).toBeCalledTimes(1);

        done();
      });
    });
  });

  describe("animation is rapidly clicked", () => {
    it("opens down() even though the element is partially expanded due to double click on up()", (done) => {
      // Visible and with explicit height.
      document.body.innerHTML = `<div data-testid="content" style="display: block; height="50px;">Content!</div>`;
      const { element } = withMockAnimation(screen.getByTestId("content"));
      const { finish } = addMockAnimation(element, "0");

      // Will toggle down():
      toggle(element).then((opened) => {
        expect(opened).toBe(null);
        expect(finish).toHaveBeenCalledTimes(1);
        expect(element.style.display).toEqual("block");

        done();
      });
    });

    it("closes up() even though the element is partially expanded due to double click on down()", (done) => {
      // Visible and with explicit height.
      document.body.innerHTML = `<div data-testid="content" style="display: block; height="50px;">Content!</div>`;
      const { element } = withMockAnimation(screen.getByTestId("content"));
      const { finish } = addMockAnimation(element, "1");

      // Will toggle down():
      toggle(element).then((opened) => {
        expect(opened).toBe(null);
        expect(finish).toHaveBeenCalledTimes(1);
        expect(element.style.display).toEqual("none");

        done();
      });
    });
  });
});

describe("custom options", () => {
  beforeEach(() => {
    document.body.innerHTML = `<div data-testid="content" style="display: none;">Content!</div>`;
  });

  it("uses default display value", (done) => {
    const { element } = withMockAnimation(screen.getByTestId("content"));
    expect(element.style.display).toEqual("none");

    down(element).then(() => {
      expect(element.style.display).toEqual("block");

      done();
    });
  });

  it("uses custom display property", (done) => {
    const { element } = withMockAnimation(screen.getByTestId("content"));
    expect(element.style.display).toEqual("none");

    down(element, { display: "flex" }).then(() => {
      expect(element.style.display).toEqual("flex");

      done();
    });
  });

  it("uses default overflow property", () => {
    const { element } = withMockAnimation(screen.getByTestId("content"));
    expect(element.style.overflow).toEqual("");

    down(element);
    expect(element.style.overflow).toEqual("hidden");
  });

  it("uses custom overflow property", () => {
    const { element } = withMockAnimation(screen.getByTestId("content"));
    expect(element.style.overflow).toEqual("");

    down(element, { overflow: "visible" });
    expect(element.style.overflow).toEqual("visible");
  });
});

describe("accessibility settings", () => {
  it("disables animation when user prefers reduced motion", (done) => {
    const { element } = withMockAnimation(screen.getByTestId("content"));

    window.matchMedia = () => {
      return {
        matches: true,
      };
    };

    up(element).then(() => {
      expect(element.animate).toHaveBeenCalledWith(expect.anything(), {
        duration: 0,
        easing: "ease",
        fill: "backwards",
      });
      done();
    });
  });
});

describe("overflow handling", () => {
  it("temporarily sets overflow to hidden", (done) => {
    document.body.innerHTML = `<div data-testid="content" style="display: none;">Content!</div>`;
    const { element } = withMockAnimation(screen.getByTestId("content"));

    expect(element.style.overflow).toEqual("");

    element.animate = () => {
      return {
        finished: new Promise((resolve) => {
          expect(element.style.overflow).toEqual("hidden");
          resolve();
        }),
      };
    };

    down(element).then(() => {
      expect(element.style.overflow).toEqual("");
      done();
    });
  });
});

describe("callback timing", () => {
  it("should fire callback after animation is complete", (done) => {
    document.body.innerHTML = `<div data-testid="content">Content!</div>`;
    const { element, getTimeCalled } = withMockAnimation(
      screen.getByTestId("content"),
      250
    );

    up(element).then(() => {
      const difference = new Date().getTime() - getTimeCalled();

      expect(difference).toBeGreaterThanOrEqual(250);
      done();
    });
  });
});