/* eslint-disable max-len */
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
import * as feedLoader from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader";
import * as useNavigationHooks from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
import { waitFor, cleanup, renderHook } from "@testing-library/react-native";
import nock from "nock";
import * as R from "ramda";
import React from "react";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import * as helpers from "../../../helpers";
import {
  noTargetScreenError,
  resolveError,
  typeWarningMessage,
} from "../useOpenSchemeHandler/const";
import { withInitialPlayerState } from "../useOpenSchemeHandler/utils";

import { log_warning } from "../../../logger";

const rivers: Record<string, Partial<ZappRiver>> = {
  A1234: {
    id: "A1234",
    home: true,
  },
  B0987: {
    id: "B0987",
  },
  C5678: {
    id: "C5678",
  },
};

const appData = {};

const contentTypes: ZappContentTypes = {
  show: {
    screen_id: "B0987",
  },
};

const endpoint = "http://feed.com";

const pipesEndpoints: ZappPipesEndpoints = {
  [`${endpoint}`]: {
    context_keys: [],
    context_obj: [],
  },
};

const navigator = {
  currentRoute: "/river/A1234",
  replace: jest.fn(),
  push: jest.fn(),
  goHome: jest.fn(),
} as any;

const mockStore = configureStore();
jest.spyOn(useNavigationHooks, "useNavigation").mockReturnValue(navigator);

const helperSpy = jest.spyOn(helpers, "handlePresentNavigation");
const feedLoaderSpy = jest.spyOn(feedLoader, "useFeedLoader");

const setEntryContext = jest.fn();

jest.doMock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
  ZappPipesEntryContext: {
    useZappPipesContext: () => [{}, setEntryContext],
  },
}));

jest.mock("../../../logger", () => ({
  log_info: jest.fn(),
  log_error: jest.fn(),
  log_warning: jest.fn(),
  log_verbose: jest.fn(),
  log_debug: jest.fn(),
}));

// to use import instead of require it's required to use jest.mock above
const { useOpenSchemeHandler } = require("../useOpenSchemeHandler");

const getWrapper = (storeProps = {}) => {
  const store = mockStore(
    R.mergeDeepRight(
      { rivers, contentTypes, pipesEndpoints, appData },
      storeProps
    )
  );

  appStore.set(store);

  const Wrapper = ({ children }: { children: React.ReactChild }) => (
    /* @ts-ignore */
    <Provider store={store}>{children}</Provider>
  );

  return Wrapper;
};

const testHook = async ({ url, query, storeProps = {}, tests }) => {
  const wrapper = getWrapper(storeProps);

  const callback = jest.fn((fn) => fn(tests));

  renderHook(() => useOpenSchemeHandler({ url, query, onFinish: callback }), {
    wrapper,
  });

  await waitFor(() => expect(callback).toHaveBeenCalled());
};

function clearMocks() {
  helperSpy.mockClear();
  feedLoaderSpy.mockClear();
  navigator.replace.mockClear();
  navigator.push.mockClear();
  navigator.goHome.mockClear();
  setEntryContext.mockClear();
}

describe("useOpenSchemeHandler", () => {
  beforeEach(clearMocks);
  afterEach(cleanup);

  describe("when query has a state param", () => {
    const entries: ZappEntry[] = [
      { id: "entry_1", type: { value: "show" }, extensions: {} },
      { id: "entry_2", type: { value: "video" } },
      { id: "entry_3", type: { value: "feed" }, screen_type: "C5678" },
      { id: 1234, type: { value: "show" } },
    ];

    const feed: ZappFeed = {
      id: "feed_1",
      type: { value: "feed" },
      entry: entries,
    };

    const feedUrl = "/feed";
    const feed_locator = `${endpoint}${feedUrl}`;

    nock(endpoint).persist().get(feedUrl).reply(200, feed);

    const url = `app://open?state=fullscreen&feed_locator=${encodeURIComponent(
      feed_locator
    )}`;

    const query = { feed_locator, state: "fullscreen" };

    it("fetches state and write it to extensions", async () => {
      const tests = () => {
        const entry = {
          ...entries[0],
          extensions: {
            initial_player_state: "fullscreen",
          },
        };

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: entry,
            navigator,
            pushScreen: true,
          })
        );
      };

      await testHook({ url, query, tests });
    });
  });

  describe("when query has a type", () => {
    beforeEach(clearMocks);
    afterEach(cleanup);

    const type = "show";
    const url = `app://open?type=${type}`;
    const query = { type };

    const entry = {
      type: { value: type },
    };

    it("doesn't log a warning", async () => {
      const tests = () => {
        expect(log_warning).not.toHaveBeenCalled();
      };

      await testHook({ url, query, tests });
    });

    it("opens the screen type-mapped to the provided value", async () => {
      const tests = () => {
        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: expect.objectContaining(entry),
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entry,
            isDeepLink: true,
          })
        );
      };

      await testHook({ url, query, tests });
    });

    it("sets the entry context with the provided parameters", async () => {
      const params = { showId: "abc", param: "value" };

      const tests = () => {
        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: expect.objectContaining(entry),
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entry,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          expect.objectContaining({
            ...params,
            extensions: { ...params, initial_player_state: "fullscreen" },
          })
        );
      };

      await testHook({ url, query: { ...query, ...params }, tests });
    });
  });

  describe("when query has a screen_id", () => {
    beforeEach(clearMocks);
    afterEach(cleanup);

    const screen_id = "C5678";
    const url = `app://open?screen_id=${screen_id}`;
    const query = { screen_id };

    it("opens the screen with the provided id", async () => {
      const tests = () => {
        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: rivers[screen_id],
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({ ...rivers[screen_id], isDeepLink: true })
        );
      };

      await testHook({ url, query, tests });
    });

    it("sets the entry context with the provided parameters", async () => {
      const params = { showId: "abc", param: "value" };

      const tests = () => {
        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: rivers[screen_id],
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...rivers[screen_id],
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          expect.objectContaining({
            ...params,
            extensions: { ...params, initial_player_state: "fullscreen" },
          })
        );
      };

      await testHook({ url, query: { ...query, ...params }, tests });
    });
  });

  describe("when query has a feed_locator", () => {
    beforeEach(clearMocks);
    afterEach(cleanup);

    const entries: ZappEntry[] = [
      { id: "entry_1", type: { value: "show" }, extensions: {} },
      { id: "entry_2", type: { value: "video" } },
      { id: "entry_3", type: { value: "feed" }, screen_type: "C5678" },
      { id: 1234, type: { value: "show" } },
    ];

    const feed: ZappFeed = {
      id: "feed_1",
      type: { value: "feed" },
      entry: entries,
    };

    const feedUrl = "/feed";
    const feed_locator = `${endpoint}${feedUrl}`;

    nock(endpoint).persist().get(feedUrl).reply(200, feed);

    const url = `app://open?feed_locator=${encodeURIComponent(feed_locator)}`;
    const query = { feed_locator };

    it("fetches the feed, sets the entryContext, and navigates to the first entry if no other parameter is provided", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[0]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: entryWithInitialPlayerState,
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entryWithInitialPlayerState,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          entryWithInitialPlayerState
        );
      };

      await testHook({ url, query, tests });
    });

    it("fetches the feed, sets the entryContext, and navigates to the entry with the provided position", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[2]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: entryWithInitialPlayerState,
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entryWithInitialPlayerState,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          entryWithInitialPlayerState
        );
      };

      await testHook({ url, query: { ...query, position: 3 }, tests });
    });

    it("fetches the feed, sets the entryContext, and navigates to the entry with the provided id", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[2]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: entryWithInitialPlayerState,
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entryWithInitialPlayerState,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          entryWithInitialPlayerState
        );
      };

      await testHook({ url, query: { ...query, id: "entry_3" }, tests });
    });

    it("fetches the feed, sets the entryContext, and navigates to the entry with the provided id as number", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[3]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: entryWithInitialPlayerState,
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entryWithInitialPlayerState,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          entryWithInitialPlayerState
        );
      };

      await testHook({ url, query: { ...query, id: "1234" }, tests });
    });

    it("fetches the feed, sets the entryContext, and opens the entry in the provided screen_id", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[1]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: rivers.B0987,
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...rivers.B0987,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          entryWithInitialPlayerState
        );
      };

      await testHook({
        url,
        query: { ...query, id: "entry_2", screen_id: "B0987" },
        tests,
      });
    });

    it("fetches the feed, sets the entryContext, and opens the entry in the screen matching the provided type", async () => {
      const tests = () => {
        const entryWithInitialPlayerState = withInitialPlayerState(
          query,
          entries[1]
        );

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: expect.objectContaining(entryWithInitialPlayerState),
            navigator,
            pushScreen: true,
          })
        );

        expect(navigator.push).toHaveBeenCalledWith(
          expect.objectContaining({
            ...entryWithInitialPlayerState,
            isDeepLink: true,
          })
        );

        expect(setEntryContext).toHaveBeenCalledWith(
          expect.objectContaining(entryWithInitialPlayerState)
        );
      };

      await testHook({
        url,
        query: { ...query, id: "entry_2", type: "show" },
        tests,
      });
    });

    it("logs an error and redirect to home if it cannot resolve the target screen", async () => {
      const tests = () => {
        expect(log_warning).toHaveBeenCalledWith(resolveError(url), {
          url,
          query: { ...query, id: "entry_2" },
          error: noTargetScreenError(url),
          stack: expect.any(String),
        });

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: null,
            navigator,
          })
        );

        expect(navigator.goHome).toHaveBeenCalled();
        expect(setEntryContext).not.toHaveBeenCalled();
      };

      await testHook({
        url,
        query: { ...query, id: "entry_2" },
        tests,
      });
    });
  });

  describe("when an error occurs", () => {
    beforeEach(clearMocks);
    afterEach(cleanup);

    const feedUrl = "/feed_error";
    const feed_locator = `${endpoint}${feedUrl}`;

    const errorMessage = "failed to resolve stream";

    nock(endpoint).get(feedUrl).replyWithError(errorMessage);

    const url = `app://open?feed_locator=${encodeURIComponent(feed_locator)}`;
    const query = { feed_locator };

    it("goes to home screen and logs a warning", async () => {
      const tests = () => {
        expect(log_warning).toHaveBeenCalledWith(resolveError(url), {
          url,
          query,
          error: errorMessage,
          stack: expect.any(String),
        });

        expect(helperSpy).toHaveBeenCalledWith(
          expect.objectContaining({
            data: null,
            navigator,
          })
        );

        expect(navigator.goHome).toHaveBeenCalled();
      };

      await testHook({ url, query, tests });
    });
  });

  describe("when query has no type, no id, no feed_locator", () => {
    beforeEach(clearMocks);
    afterEach(cleanup);

    const url = "app://open?param=value";
    const query = { param: "value" };

    it("logs a warning", async () => {
      const tests = () => {
        expect(log_warning).toBeCalledWith(
          typeWarningMessage(url),
          expect.objectContaining({ url, query })
        );
      };

      await testHook({ url, query, tests });
    });
  });
});
