import { isAmdEnv, isRequireJsEnv } from "../lib/amd-utils";
import { constants, FL_NAMESPACE } from "../constants";
import assetLoader from "@braintree/asset-loader";
import loadAxo from "../loadAxoScript";
import { AxoSupportedPlatforms } from "../types";
import { btModulesLoadConfig } from "../lib/safeLoadBtModule/types";

jest.mock("@braintree/asset-loader", () => {
  return {
    loadScript: jest.fn(),
    loadStylesheet: jest.fn(),
  };
});
jest.mock("../lib/amd-utils");
const mockIsAmdEnv = isAmdEnv as jest.MockedFn<typeof isAmdEnv>;
const mockIsRequireJsEnv = isRequireJsEnv as jest.MockedFn<
  typeof isRequireJsEnv
>;

describe("loadAxoScript", () => {
  let testContext;
  const version = "3.96";
  const originalWindow = window;
  const originalPerformance = performance;

  beforeEach(() => {
    testContext = {};
    testContext.mockLoadResponse = {
      metadata: {
        localeUrl: "https://www.paypalobjects.com/connect-boba/locales/",
      },
    };
    testContext.client = {
      getVersion: () => {
        return version;
      },
    };

    // Mock performance API
    Object.defineProperty(globalThis, "performance", {
      value: {
        mark: jest.fn(),
        measure: jest.fn(),
      },
    });
  });

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

    Object.defineProperty(globalThis, "performance", {
      value: originalPerformance,
    });
  });

  describe("Unhappy path", () => {
    it("rejects promise if platform is invalid", async () => {
      const promise = loadAxo({
        // @ts-expect-error for test purposes
        platform: "Invalid Platform",
        btSdkVersion: version,
        minified: true,
      });
      await expect(promise).rejects.toEqual(
        new Error("unsupported axo platform")
      );
    });
  });

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

    it("should call loadScript with the minified URL when specified", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
      });

      expect(promise).resolves.toStrictEqual(testContext.mockLoadResponse);
      expect(assetLoader.loadScript).toHaveBeenCalled();
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `${constants.CDNX_PROD}/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });

    it("should load the unminified URL when specified", () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: false,
      });

      expect(promise).resolves.toStrictEqual(testContext.mockLoadResponse);
      expect(assetLoader.loadScript).toHaveBeenCalled();
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `${constants.CDNX_PROD}/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.unminified}.js`,
        })
      );
    });

    it("should default to the minified script when no environment is specified", () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
      });
      expect(promise).resolves.toStrictEqual(testContext.mockLoadResponse);
      expect(assetLoader.loadScript).toHaveBeenCalled();
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `${constants.CDNX_PROD}/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });

    it("should load the bundle override URL when specified", () => {
      const bundleId = "2bc4343c92655";
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        metadata: { bundleIdOverride: bundleId },
      });

      expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.LOCALE_PATH}`,
        },
      });
      expect(assetLoader.loadScript).toHaveBeenCalled();
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });

    it("should call loadScript with the minified URL and bundle override URL when specified", async () => {
      const bundleId = "9df9369c92655";
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
        metadata: { bundleIdOverride: bundleId },
      });
      expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.LOCALE_PATH}`,
        },
      });
      expect(assetLoader.loadScript).toHaveBeenCalled();
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });

    it("should load Bt hcf and axo when platform is bt", () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
      });

      expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `${constants.CDNX_PROD}/${constants.LOCALE_PATH}`,
        },
      });
      expect(assetLoader.loadScript).toHaveBeenCalledTimes(2);
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          id: `hcf-${version}`,
          src: `https://js.braintreegateway.com/web/${version}/js/hosted-fields.min.js`,
        })
      );
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `${constants.CDNX_PROD}/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });

    it("should load Bt client, hcf and Axo when platform is ppcp", () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.PPCP,
        btSdkVersion: version,
        minified: true,
      });

      expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `${constants.CDNX_PROD}/${constants.LOCALE_PATH}`,
        },
      });

      expect(assetLoader.loadScript).toHaveBeenCalledTimes(3);

      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          id: `client-${version}`,
          src: `https://js.braintreegateway.com/web/${version}/js/client.min.js`,
        })
      );
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          id: `hcf-${version}`,
          src: `https://js.braintreegateway.com/web/${version}/js/hosted-fields.min.js`,
        })
      );
      expect(assetLoader.loadScript).toHaveBeenCalledWith(
        expect.objectContaining({
          src: `${constants.CDNX_PROD}/${constants.AXO_ASSET_PATH}/${constants.AXO_ASSET_NAME.minified}.js`,
        })
      );
    });
  });

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

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

    afterEach(() => {
      Object.defineProperty(globalThis, "require", {
        value: originalRequire,
      });
    });

    describe("with Require.js specifically", () => {
      let mockRequireJs;
      let mockRequireJsConfig;

      beforeEach(() => {
        mockIsRequireJsEnv.mockReturnValue(true);
        mockRequireJs = jest.fn();
        mockRequireJsConfig = jest.fn();
        mockRequireJs.config = mockRequireJsConfig;
        Object.defineProperty(globalThis, "requirejs", {
          value: mockRequireJs,
          writable: true,
        });
      });

      afterEach(() => {
        Object.defineProperty(globalThis, "requirejs", {
          value: undefined,
        });
      });

      it("configures Require.js with the fastlane namespace", async () => {
        await loadAxo({
          platform: AxoSupportedPlatforms.BT,
          btSdkVersion: version,
        });

        expect(mockRequireJsConfig).toHaveBeenCalledWith({
          paths: {
            [FL_NAMESPACE]: "https://www.paypalobjects.com/connect-boba",
          },
        });
      });
    });

    it("should load the minified AMD modules", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
      });

      await expect(promise).resolves.toStrictEqual(
        testContext.mockLoadResponse
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [btModulesLoadConfig.hostedFields.amdModule.minified],
        expect.any(Function),
        expect.any(Function)
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo.min`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should load the unminified AMD modules", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: false,
      });

      await expect(promise).resolves.toStrictEqual(
        testContext.mockLoadResponse
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should default to the minified script when no environment is specified", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
      });

      await expect(promise).resolves.toStrictEqual(
        testContext.mockLoadResponse
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo.min`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should load the bundle override URL when specified", async () => {
      const bundleId = "2bc4343c92655";
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        metadata: { bundleIdOverride: bundleId },
      });

      await expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.LOCALE_PATH}`,
        },
      });
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo.min`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should call loadScript with the minified URL and bundle override URL when specified", async () => {
      const bundleId = "9df9369c92655";
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
        metadata: { bundleIdOverride: bundleId },
      });

      await expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `https://cdn-${bundleId}.static.engineering.dev.paypalinc.com/${constants.LOCALE_PATH}`,
        },
      });
      expect(assetLoader.loadScript).not.toHaveBeenCalled();
    });

    it("should load Bt hcf and axo when platform is bt", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.BT,
        btSdkVersion: version,
        minified: true,
      });

      await expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `${constants.CDNX_PROD}/${constants.LOCALE_PATH}`,
        },
      });

      expect(mockRequire).toHaveBeenCalledTimes(2);
      expect(mockRequire).toHaveBeenCalledWith(
        [`${btModulesLoadConfig.hostedFields.amdModule.minified}`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo.min`],
        expect.any(Function),
        expect.any(Function)
      );
    });

    it("should load Bt client, hcf and Axo when platform is ppcp", async () => {
      const promise = loadAxo({
        platform: AxoSupportedPlatforms.PPCP,
        btSdkVersion: version,
        minified: true,
      });

      await expect(promise).resolves.toStrictEqual({
        metadata: {
          localeUrl: `${constants.CDNX_PROD}/${constants.LOCALE_PATH}`,
        },
      });

      expect(mockRequire).toHaveBeenCalledTimes(3);
      expect(mockRequire).toHaveBeenCalledWith(
        [`${btModulesLoadConfig.hostedFields.amdModule.minified}`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${btModulesLoadConfig.client.amdModule.minified}`],
        expect.any(Function),
        expect.any(Function)
      );
      expect(mockRequire).toHaveBeenCalledWith(
        [`${FL_NAMESPACE}/axo.min`],
        expect.any(Function),
        expect.any(Function)
      );
    });
  });
});
