import { safeLoadBtModule } from "..";
import { BT_NAMESPACE, BtModule, btModulesLoadConfig } from "../types";
import assetLoader from "@braintree/asset-loader";
import { isAmdEnv } from "../../amd-utils";

jest.mock("@braintree/asset-loader", () => {
  return {
    loadScript: jest.fn(),
    loadStylesheet: jest.fn(),
  };
});

jest.mock("../../amd-utils");

const mockIsAmdEnv = isAmdEnv as jest.MockedFn<typeof isAmdEnv>;

describe("safeLoadBtModule", () => {
  const version = "3.97.2";
  describe("in a non-AMD environment", () => {
    beforeAll(() => {
      mockIsAmdEnv.mockReturnValue(false);
    });

    it("should attempt to load BT module", async () => {
      const { id, script } = btModulesLoadConfig.client;

      await safeLoadBtModule(btModulesLoadConfig.client, version);

      const scriptSrc = `https://js.braintreegateway.com/web/${version}/js/${script.minified}`;
      const loadScriptOptions = {
        id: `${id}-${version}`,
        src: scriptSrc,
      };

      expect(assetLoader.loadScript).toHaveBeenCalledWith(loadScriptOptions);
    });

    it("should not load BT module if same version already exists", async () => {
      window.braintree = {
        [BtModule.Client]: {
          create: jest.fn(),
          VERSION: version,
        },
      };

      const loadResp = await safeLoadBtModule(
        btModulesLoadConfig.client,
        version
      );

      expect(assetLoader.loadScript).not.toHaveBeenCalled();
      expect(loadResp).toBe(true);
    });

    it("should not load BT module and return error if module on different version already exists", async () => {
      window.braintree = {
        [BtModule.Client]: {
          create: jest.fn(),
          VERSION: "3.96.0",
        },
      };

      try {
        await safeLoadBtModule(btModulesLoadConfig.client, version);
      } catch (error) {
        expect(error).toStrictEqual(
          new Error(
            "client already loaded with version 3.96.0 cannot load version 3.97.2"
          )
        );
      }

      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should not load BT module and return error if no version specified", async () => {
      window.braintree = {
        [BtModule.Client]: {
          create: jest.fn(),
          VERSION: "3.96.0",
        },
      };

      try {
        await safeLoadBtModule(btModulesLoadConfig.client, "");
      } catch (error) {
        expect(error).toStrictEqual(
          new Error("Attempted to load client without specifying version")
        );
      }

      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });
  });

  describe("in an AMD environment", () => {
    const originalWindow = window;
    let mockRequire: jest.Mock;

    beforeAll(() => {
      mockIsAmdEnv.mockReturnValue(true);
      mockRequire = jest.fn().mockImplementation((deps, cb) => {
        cb();
      });
      Object.defineProperty(globalThis, "window", {
        value: {
          require: mockRequire,
        },
      });
    });

    afterAll(() => {
      Object.defineProperty(globalThis, "window", {
        value: originalWindow,
      });
    });

    it("should attempt to load BT module", async () => {
      await safeLoadBtModule(btModulesLoadConfig.client, version);

      expect(assetLoader.loadScript).not.toHaveBeenCalled();
      expect(mockRequire).toHaveBeenCalledWith(
        [`${BT_NAMESPACE}/client.min`],
        expect.any(Function),
        expect.any(Function)
      );
    });

    it("should reject the promise if the require call fails", async () => {
      const mockError = new Error("Boom!");
      mockRequire.mockImplementationOnce((deps, cb, errback) => {
        errback(mockError);
      });

      try {
        await safeLoadBtModule(btModulesLoadConfig.client, version);
      } catch (error) {
        expect(error).toBe(mockError);
      }

      expect(mockRequire).toHaveBeenCalledWith(
        [`${BT_NAMESPACE}/client.min`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });
  });
});
