const layoutId = "A1234";

const layout = {
  id: layoutId,
  screens: [
    {
      id: "screen1",
      name: "home",
      general: { icon: "http://img.com/icon.png" },
      home: true,
      home_offline: false,
      supports_offline: false,
    },
    {
      id: "screen2",
      name: "show screen",
      general: {},
      navigations: [
        {
          category: "tabs",
          nav_items: [
            { target: "screen3", asset: "http://img.com/tab_asset.png" },
          ],
        },
      ],
      home: false,
      home_offline: false,
      supports_offline: false,
    },
    {
      id: "screen3",
      name: "other screen",
      general: {},
      home: false,
      home_offline: false,
      supports_offline: false,
      styles: {
        failing_asset: "http://img.com/failing_asset.png",
      },
    },
  ],
  content_types: {
    type: {
      screen_id: "screen1",
    },
  },
};

const pluginConfigurations = [
  {
    plugin: {
      api: {},
      dependency_name: "plugin_1",
      name: "plugin 1",
      type: "general" as PluginType,
      identifier: "plugin_1",
      dependency_version: "0.0.1",
    },
    configuration_json: { plugin_asset: "http://img.com/plugin_asset.png" },
  },
];

const cellStyles = {
  cellId1: {
    plugin_identifier: "cell-style",
    configuration: {
      assets: { cell_asset: "http://img.com/cell_asset.png" },
      styles: {},
    },
  },
};

let assetCacheEnabled = true;
let isTV = false;

const offlineAssetsBridge = jest.requireActual(
  "@applicaster/zapp-react-native-bridge/OfflineAssets"
);

const mockedStoreFiles = (files) => {
  const result = files.reduce((acc, { url, file }) => {
    const success = !url.includes("fail");

    return { ...acc, [file]: success };
  }, {});

  return Promise.resolve(result);
};

jest.mock("@applicaster/zapp-react-native-bridge/OfflineAssets", () => ({
  isAssetCacheEnabled: jest.fn(() => assetCacheEnabled),
  getNativeRootPath: jest.fn(() => Promise.resolve("/path")),
  storeFiles: jest.fn((files) => mockedStoreFiles(files)),
  collectAssets: jest.fn(offlineAssetsBridge.collectAssets),
  remapAssetPath: jest.fn(offlineAssetsBridge.remapAssetPath),
  fileIsSaved: jest.fn(offlineAssetsBridge.fileIsSaved),
}));

jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
  isTV: jest.fn(() => isTV),
  platformSelect: jest.fn(
    (specs) => specs.ios || specs.android || specs.default
  ),
  isApplePlatform: jest.fn(() => true),
}));

const { CacheManager } = require("../CacheManager");

const {
  storeFiles,
} = require("@applicaster/zapp-react-native-bridge/OfflineAssets");

const getFilesToCache = jest.spyOn(CacheManager.instance, "getFilesToCache");

const clearCache = jest
  .spyOn(CacheManager.instance, "clearCache")
  .mockImplementation(jest.fn());

const saveCacheData = jest
  .spyOn(CacheManager.instance, "saveCacheData")
  .mockImplementation(jest.fn());

const { cacheAssets, getLayoutAssets } = require("..");

function clearMocks() {
  getFilesToCache.mockClear();
  clearCache.mockClear();
  saveCacheData.mockClear();
  storeFiles.mockClear();
}

describe("getLayoutAssets", () => {
  it("returns the list of assets in the layout", async () => {
    await expect(
      getLayoutAssets({ layout, pluginConfigurations, cellStyles })
    ).resolves.toMatchSnapshot();
  });
});

describe("asset cache", () => {
  describe("when native module doesn't exist", () => {
    beforeEach(() => {
      assetCacheEnabled = false;

      clearMocks();
    });

    afterAll(() => {
      assetCacheEnabled = true;
    });

    it("it returns the provided config", async () => {
      const layoutData = {
        layout,
        pluginConfigurations,
        cellStyles,
      };

      await expect(cacheAssets(layoutData)).resolves.toEqual(layoutData);
    });
  });

  describe("when running on a TV platform", () => {
    beforeEach(() => {
      isTV = true;

      clearMocks();
    });

    afterAll(() => {
      isTV = false;
    });

    it("it returns the provided config", async () => {
      const layoutData = {
        layout,
        pluginConfigurations,
        cellStyles,
      };

      await expect(cacheAssets(layoutData)).resolves.toEqual(layoutData);
    });
  });

  describe("when layoutId is undefined", () => {
    beforeEach(clearMocks);

    it("returns the provided configuration", async () => {
      const layoutWithNoId = {
        ...layout,
        id: null,
      };

      const layoutData = {
        layout: layoutWithNoId,
        pluginConfigurations,
        cellStyles,
      };

      await expect(cacheAssets(layoutData)).resolves.toEqual(layoutData);
    });
  });

  describe("when there are files to cache", () => {
    const removedFiles = [];

    beforeEach(async () => {
      clearMocks();

      const { assetFiles: newFiles } = await getLayoutAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      getFilesToCache.mockImplementation(() =>
        Promise.resolve({
          removedFiles,
          newFiles,
          unchangedFiles: [],
        })
      );
    });

    it("stores the new files", async () => {
      await cacheAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      const { assetFiles } = await getLayoutAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      expect(clearCache).not.toHaveBeenCalled();
      expect(storeFiles).toHaveBeenCalledWith(assetFiles);

      expect(saveCacheData).toHaveBeenCalledWith(
        layoutId,
        assetFiles.filter(({ url }) => !url.includes("fail"))
      );
    });

    it("returns the remapped layout files", async () => {
      const result = await cacheAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      expect(result).toMatchSnapshot();
    });
  });

  describe("when there are no files to cache", () => {
    beforeEach(() => {
      clearMocks();

      getFilesToCache.mockImplementation(() =>
        Promise.resolve({
          removedFiles: [],
          newFiles: [],
          unchangedFiles: [],
        })
      );
    });

    it("doesn't cache any new file", () => {
      expect(clearCache).not.toHaveBeenCalled();
      expect(storeFiles).not.toHaveBeenCalled();
      expect(saveCacheData).not.toHaveBeenCalled();
    });

    it("returns the remapped layout files", async () => {
      await expect(
        cacheAssets({ layout, pluginConfigurations, cellStyles })
      ).resolves.toMatchSnapshot();
    });
  });

  describe("when only a few files changed", () => {
    beforeEach(async () => {
      clearMocks();

      const { assetFiles } = await getLayoutAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      const [removedFiles, ...newFiles] = assetFiles;

      getFilesToCache.mockImplementation(() =>
        Promise.resolve({
          removedFiles: [removedFiles],
          newFiles,
          unchangedFiles: [],
        })
      );
    });

    it("stores the new files and removes the obsolete ones", async () => {
      await cacheAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      const { assetFiles } = await getLayoutAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      const [removedFiles, ...newFiles] = assetFiles;

      expect(clearCache).toHaveBeenCalledWith([removedFiles]);

      expect(storeFiles).toHaveBeenCalledWith(newFiles);

      expect(saveCacheData).toHaveBeenCalledWith(
        layoutId,
        newFiles.filter(({ url }) => !url.includes("fail"))
      );
    });

    it("returns the remapped layout files", async () => {
      const result = await cacheAssets({
        layout,
        pluginConfigurations,
        cellStyles,
      });

      expect(result).toMatchSnapshot();
    });
  });
});
