import expect from 'expect';

import createTestState from 'mtest/helpers/createTestState';
import { GAME_1, GAME_2, GAME_3 } from 'mtest/fetchMocks/games';

import { Platform, Location } from 'mweb/common/reducers/app';
import { OS, Browser } from 'mweb/common/reducers/device';
import { VideoType } from 'mweb/common/reducers/data/baseVideoDetails';
import { VODDetails } from 'mweb/common/reducers/data/vods';
import {
  buildRootReducer,
  statusInternalError,
  statusNotFound,
  gameList,
  getCurrentChannelForChannelViewer,
  getCurrentChannelForVODViewer,
  getCurrentChannelForProfile,
  getCurrentChannelForChatEmbed,
  getCurrentChannel,
  getCurrentVODForVODViewer,
  getIsOptedOut,
  getVODDetails,
  getVODsForChannel,
  getClipsForChannel,
  VODSortTarget,
  getCurrentEventForEventDetails,
  statusSuccess,
  isChatReady,
  isFixedTopNav,
  isSupportedMobileOS,
  isOnClient,
  returnStatus,
  RootState,
  StatusCode,
  showOpenInApp,
} from 'mweb/common/reducers/root';
import { MOBILE_OPT_OUT_COOKIE_NAME } from 'mweb/common/utils/cookie';
import { Enum } from 'mweb/common/utils/enum';

describe('root reducer', () => {
  it('does not mutate the state & returns it for unhandled actions', () => {
    // Force any to make sure the code acts as expected when endrunning around
    // the type system.
    const notAction: any = {
      type: 'THIS IS NOT AN ACTION',
      payload: {
        like: 'whatever',
        it: 'does not matter',
      },
    };

    const state = createTestState({
      pages: {
        channelViewer: {
          currentChannel: 'voxel',
        },
      },
    });

    Object.freeze(state);
    expect(buildRootReducer()(state, notAction)).toEqual(state);
  });
});

describe('getIsOptOut', () => {
  it('properly extracts opt out value', () => {
    const state = createTestState({
      app: {
        mobileOptOut: MOBILE_OPT_OUT_COOKIE_NAME,
      },
    });
    expect(getIsOptedOut(state)).toEqual(MOBILE_OPT_OUT_COOKIE_NAME);
  });
});

describe('isSupportedMobileOS', () => {
  interface MobileOSTestCase {
    os: OS;
    expected: boolean;
  }
  const tests: MobileOSTestCase[] = [
    { os: OS.Android, expected: true },
    { os: OS.iOS, expected: true },
    { os: OS.Microsoft, expected: false },
    { os: OS.Other, expected: false },
    { os: OS.Unknown, expected: false },
  ];

  tests.forEach(({ os, expected }) => {
    it(`indicates whether ${os} is mobile`, () => {
      const state = createTestState({
        device: { os },
      });
      expect(isSupportedMobileOS(state)).toEqual(expected);
    });
  });
});

describe('isOnClient', () => {
  it('indicates on client when on client', () => {
    const state = createTestState({ app: { platform: Platform.Client } });
    expect(isOnClient(state)).toEqual(true);
  });

  it('indicates on server when on server', () => {
    const state = createTestState({ app: { platform: Platform.Server } });
    expect(isOnClient(state)).toEqual(false);
  });
});

describe('isChatReady', () => {
  it('indicates that no messages means chat is not ready', () => {
    expect(
      isChatReady(({ chat: { messages: [] } } as any) as RootState),
    ).toEqual(false);
  });

  it('indicates that some messages means chat is not ready', () => {
    expect(
      isChatReady(({ chat: { messages: [{}] } } as any) as RootState),
    ).toEqual(true);
  });
});

describe('isFixedTopNav', () => {
  it('indicates that the channel page is a fixed title bar location', () => {
    expect(
      isFixedTopNav({ app: { location: Location.Channel } } as RootState),
    ).toEqual(false);
  });

  it('indicates that the vod page is a fixed title bar location', () => {
    expect(
      isFixedTopNav({ app: { location: Location.VOD } } as RootState),
    ).toEqual(false);
  });

  it('indicates that the directory page is not a fixed title bar location', () => {
    expect(
      isFixedTopNav({ app: { location: Location.DirectoryGame } } as RootState),
    ).toEqual(true);
  });
  it('indicates that the main directory page is not a fixed title bar location', () => {
    expect(
      isFixedTopNav({
        app: { location: Location.DirectoryMainGame },
      } as RootState),
    ).toEqual(true);
  });
});

describe('statusNotFound', () => {
  it('returns true when not found', () => {
    expect(
      statusNotFound({ app: { status: StatusCode.NOT_FOUND } } as RootState),
    ).toEqual(true);
  });

  it('returns true when bad request', () => {
    expect(
      statusNotFound({ app: { status: StatusCode.BAD_REQUEST } } as RootState),
    ).toEqual(true);
  });

  it('returns true when unprocessable', () => {
    expect(
      statusNotFound({
        app: { status: StatusCode.UNPROCESSABLE },
      } as RootState),
    ).toEqual(true);
  });

  it('returns false when found', () => {
    expect(
      statusNotFound({ app: { status: StatusCode.SUCCESS } } as RootState),
    ).toEqual(false);
  });
});

describe('statusSuccess', () => {
  it('returns true when status is default', () => {
    expect(statusSuccess(createTestState({}))).toEqual(true);
  });

  it('returns false when status is less than 200', () => {
    expect(statusSuccess(createTestState({ app: { status: 100 } }))).toEqual(
      false,
    );
  });

  it('returns true when status is success', () => {
    expect(
      statusSuccess(createTestState({ app: { status: StatusCode.SUCCESS } })),
    ).toEqual(true);
  });

  it('returns false when status is above 299', () => {
    expect(
      statusSuccess(
        createTestState({ app: { status: StatusCode.INTERNAL_SERVER_ERROR } }),
      ),
    ).toEqual(false);
  });
});

describe('statusInternalError', () => {
  it('returns false when statusNotFound and statusNotSuccess', () => {
    expect(
      statusInternalError(
        createTestState({ app: { status: StatusCode.NOT_FOUND } }),
      ),
    ).toEqual(false);
  });

  it('returns true when not statusNotFound and statusNotSuccess', () => {
    expect(
      statusInternalError(
        createTestState({ app: { status: StatusCode.INTERNAL_SERVER_ERROR } }),
      ),
    ).toEqual(true);
  });

  it('returns false when neither statusNotSuccess nor statusNotFound', () => {
    expect(
      statusInternalError(
        createTestState({ app: { status: StatusCode.SUCCESS } }),
      ),
    ).toEqual(false);
  });
});

describe('returnStatus', () => {
  const properStatusTestCases: Location[] = [
    Location.Channel,
    Location.VOD,
    Location.EventDetails,
    Location.DirectoryGame,
    Location.DirectoryMainGame,
  ];

  properStatusTestCases.forEach(location => {
    describe(`for location ${location}`, () => {
      it('handles NOT_FOUND', () => {
        expect(
          returnStatus({
            app: { status: StatusCode.NOT_FOUND, location },
          } as RootState),
        ).toEqual(StatusCode.NOT_FOUND);
      });

      it('handles INTERNAL_SERVER_ERROR', () => {
        expect(
          returnStatus({
            app: {
              status: StatusCode.INTERNAL_SERVER_ERROR,
              location,
            },
          } as RootState),
        ).toEqual(StatusCode.INTERNAL_SERVER_ERROR);
      });

      it('handles SUCCESS', () => {
        expect(
          returnStatus({
            app: { status: StatusCode.SUCCESS, location },
          } as RootState),
        ).toEqual(StatusCode.SUCCESS);
      });
    });
  });

  const alwaysSuccessTestCases: Location[] = [
    Location.Unknown,
    Location.Upsell,
  ];

  alwaysSuccessTestCases.forEach(location => {
    describe(`for location ${location}`, () => {
      it(`SUCCESS for ${location} when SUCCESS`, () => {
        expect(
          returnStatus({
            app: { status: StatusCode.SUCCESS, location },
          } as RootState),
        ).toEqual(StatusCode.SUCCESS);
      });

      it(`always SUCCESS for ${location} regardless of INTERNAL_SERVER_ERROR`, () => {
        expect(
          returnStatus({
            app: { status: StatusCode.INTERNAL_SERVER_ERROR, location },
          } as RootState),
        ).toEqual(StatusCode.SUCCESS);
      });

      it(`always SUCCESS for ${location} regardless of NOT_FOUND`, () => {
        expect(
          returnStatus({
            app: { status: StatusCode.NOT_FOUND, location },
          } as RootState),
        ).toEqual(StatusCode.SUCCESS);
      });
    });
  });

  describe('for unexpected locations', () => {
    expect(
      returnStatus(({
        app: {
          status: StatusCode.INTERNAL_SERVER_ERROR,
          location: 'blah blah blah',
        },
      } as any) as RootState),
    ).toEqual(StatusCode.INTERNAL_SERVER_ERROR);
  });
});

describe('gameList', () => {
  it('returns a properly ordered list of games', () => {
    const state = createTestState({
      data: {
        games: {
          gameDetails: {
            [GAME_1.name]: GAME_1,
            [GAME_2.name]: GAME_2,
            [GAME_3.name]: GAME_3,
          },
        },
      },
    });

    expect(gameList(state)).toEqual([GAME_3, GAME_2, GAME_1]);
  });
});

describe('current channel selectors', () => {
  describe('getCurrentChannelForChannelViewer', () => {
    const channelDetails = {
      displayName: 'BigAndy',
      game: 'game',
      id: '1',
      name: 'bigandy',
      preview: '',
      status: '',
      viewerCount: 1,
    };

    it('returns the current loaded channel', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              [channelDetails.name]: channelDetails,
            },
          },
        },
        pages: {
          channelViewer: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForChannelViewer(state)).toEqual(channelDetails);
    });

    it('returns undefined if currentChannel not in channelDetails', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              biggestandy: { name: 'biggestandy' },
            },
          },
        },
        pages: {
          channelViewer: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForChannelViewer(state)).toEqual(undefined);
    });
  });

  describe('getCurrentChannelForVODViewer', () => {
    const channelDetails = {
      displayName: 'BigAndy',
      game: 'game',
      id: '1',
      name: 'bigandy',
      preview: '',
      status: '',
      viewerCount: 1,
    };
    const vodDetails = {
      vodID: 'v123',
      channel: channelDetails.name,
    };

    it('returns the current loaded channel', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              [channelDetails.name]: channelDetails,
            },
          },
          vods: {
            vodDetails: {
              [vodDetails.vodID]: vodDetails,
            },
          },
        },
        pages: {
          vodViewer: {
            currentVODid: vodDetails.vodID,
          },
        },
      });

      expect(getCurrentChannelForVODViewer(state)).toEqual(channelDetails);
    });

    it('returns undefined if currentChannel not in channelDetails', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              biggestandy: { name: 'biggestandy' },
            },
          },
          vods: {
            vodDetails: {
              [vodDetails.vodID]: vodDetails,
            },
          },
        },
        pages: {
          vodViewer: {
            currentVODid: vodDetails.vodID,
          },
        },
      });

      expect(getCurrentChannelForVODViewer(state)).toEqual(undefined);
    });
  });

  describe('getCurrentChannelForProfile', () => {
    const channelDetails = {
      displayName: 'BigAndy',
      game: 'game',
      id: '1',
      name: 'bigandy',
      preview: '',
      status: '',
      viewerCount: 1,
    };

    it('returns the current loaded channel', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              [channelDetails.name]: channelDetails,
            },
          },
        },
        pages: {
          channelProfile: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForProfile(state)).toEqual(channelDetails);
    });

    it('returns undefined if currentChannel not in channelDetails', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              biggestandy: { name: 'biggestandy' },
            },
          },
        },
        pages: {
          channelProfile: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForProfile(state)).toEqual(undefined);
    });
  });

  describe('getCurrentChannelForChatEmbed', () => {
    const channelDetails = {
      displayName: 'BigAndy',
      game: 'game',
      id: '1',
      name: 'bigandy',
      preview: '',
      status: '',
      viewerCount: 1,
    };

    it('returns the current loaded channel', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              [channelDetails.name]: channelDetails,
            },
          },
        },
        pages: {
          chatEmbed: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForChatEmbed(state)).toEqual(channelDetails);
    });

    it('returns undefined if currentChannel not in channelDetails', () => {
      const state = createTestState({
        data: {
          channels: {
            channelDetails: {
              biggestandy: { name: 'biggestandy' },
            },
          },
        },
        pages: {
          chatEmbed: {
            currentChannel: channelDetails.name,
          },
        },
      });

      expect(getCurrentChannelForChatEmbed(state)).toEqual(undefined);
    });
  });

  describe('getCurrentChannel', () => {
    const channelDetails = {
      displayName: 'BigAndy',
      name: 'bigandy',
    };
    const channelDetails2 = {
      displayName: 'BiggerAndy',
      name: 'biggerandy',
    };
    const channelDetails3 = {
      displayName: 'BiggestAndy',
      name: 'biggestandy',
    };
    const vodDetails = {
      vodID: 'v123',
      channel: 'biggerandy',
    };

    const TEST_DATA = [
      {
        name: 'channel',
        expected: channelDetails,
        stateSeed: {
          app: {
            location: Location.Channel,
          },
          data: {
            channels: {
              channelDetails: {
                [channelDetails.name]: channelDetails,
              },
            },
          },
          pages: {
            channelViewer: {
              currentChannel: channelDetails.name,
            },
          },
        },
      },
      {
        name: 'vod',
        expected: channelDetails2,
        stateSeed: {
          app: {
            location: Location.VOD,
          },
          data: {
            channels: {
              channelDetails: {
                [channelDetails2.name]: channelDetails2,
              },
            },
            vods: {
              vodDetails: {
                [vodDetails.vodID]: vodDetails,
              },
            },
          },
          pages: {
            vodViewer: {
              currentVODid: vodDetails.vodID,
            },
          },
        },
      },
      {
        name: 'profile',
        expected: channelDetails3,
        stateSeed: {
          app: {
            location: Location.ChannelProfile,
          },
          data: {
            channels: {
              channelDetails: {
                [channelDetails3.name]: channelDetails3,
              },
            },
          },
          pages: {
            channelProfile: {
              currentChannel: channelDetails3.name,
            },
          },
        },
      },
      {
        name: 'chatEmbed',
        expected: channelDetails3,
        stateSeed: {
          app: {
            location: Location.ChatEmbed,
          },
          data: {
            channels: {
              channelDetails: {
                [channelDetails3.name]: channelDetails3,
              },
            },
          },
          pages: {
            chatEmbed: {
              currentChannel: channelDetails3.name,
            },
          },
        },
      },
    ];

    TEST_DATA.forEach(testCase => {
      it(`finds loaded channel for ${testCase.name}`, () => {
        const state = createTestState(testCase.stateSeed);
        expect(getCurrentChannel(state)).toEqual(testCase.expected);
      });
    });

    it('returns undefined for non channel locations', () => {
      const locations = [
        Location.Upsell,
        Location.DirectoryMainGame,
        Location.DirectoryGame,
        Location.EventDetails,
        Location.Upsell,
        Location.Unknown,
      ];
      const state = {
        app: {
          location: Location.Channel,
        },
        data: {
          channels: {
            channelDetails: {
              [channelDetails.name]: channelDetails,
            },
          },
          vods: {
            vodDetails: {
              [vodDetails.vodID]: vodDetails,
            },
          },
        },
        pages: {
          channelProfile: {
            currentChannel: channelDetails.name,
          },
          vodViewer: {
            currentVODid: vodDetails.vodID,
          },
          channelViewer: {
            currentChannel: channelDetails.name,
          },
        },
      };
      locations.forEach(location => {
        state.app.location = location;
        expect(getCurrentChannel(createTestState(state))).toEqual(undefined);
      });
    });
  });
});

describe('getCurrentVODForVODViewer', () => {
  const vodDetails = {
    id: '345',
  };

  it('finds loaded VOD when present', () => {
    const state = createTestState({
      data: {
        vods: {
          vodDetails: {
            [vodDetails.id]: vodDetails,
          },
        },
      },
      pages: {
        vodViewer: {
          currentVODid: vodDetails.id,
        },
      },
    });

    expect(getCurrentVODForVODViewer(state)).toEqual(vodDetails);
  });

  it('returns undefined when vod not present', () => {
    const state = createTestState({
      data: {
        vods: {
          vodDetails: {
            [vodDetails.id]: vodDetails,
          },
        },
      },
      pages: {
        vodViewer: {
          currentVODid: '789',
        },
      },
    });

    expect(getCurrentVODForVODViewer(state)).toEqual(undefined);
  });
});

describe('getVODDetails', () => {
  it('returns details for a loaded vod', () => {
    const vodDetails: Partial<VODDetails> = {
      videoType: VideoType.Archive,
      channel: 'channel',
      description: 'Channel VOD',
      game: 'game',
      id: '1',
      title: '',
    };

    const state = createTestState({
      data: {
        vods: {
          vodDetails: {
            [vodDetails.id!]: vodDetails,
          },
        },
      },
    });

    expect(getVODDetails(state, vodDetails.id!)).toEqual(vodDetails);
  });

  it('returns undefined for non-loaded vod', () => {
    const state = createTestState();

    expect(getVODDetails(state, '1')).toEqual(undefined);
  });
});

describe('getCurrentEventForEventDetails', () => {
  const event = {
    id: '123',
    title: 'Riding the bus',
  };
  it('returns details for current event', () => {
    const state = createTestState({
      data: {
        events: {
          eventDetails: {
            [event.id]: event,
          },
        },
      },
      pages: {
        eventDetails: {
          currentEventID: '123',
        },
      },
    });
    expect(getCurrentEventForEventDetails(state)).toEqual(event);
  });

  it('returns undefined if not current event', () => {
    const state = createTestState({
      data: {
        events: {
          eventDetails: {
            [event.id]: event,
          },
        },
      },
      pages: {
        eventDetails: {
          currentEventID: '321',
        },
      },
    });
    expect(getCurrentEventForEventDetails(state)).toEqual(undefined);
  });
});

describe('getVODsForChannel', () => {
  const vod1: Partial<VODDetails> = {
    channel: 'channel',
    videoType: VideoType.Archive,
    date: 0,
  };

  const vod2: Partial<VODDetails> = {
    channel: 'channel',
    videoType: VideoType.Archive,
    date: 1,
  };

  const vod3: Partial<VODDetails> = {
    channel: 'channel',
    videoType: VideoType.Highlight,
    date: 2,
  };

  const vod4: Partial<VODDetails> = {
    channel: 'not channel',
    videoType: VideoType.Archive,
    date: 2,
  };

  const state = createTestState({
    data: {
      vods: {
        vodDetails: {
          vod1,
          vod2,
          vod3,
          vod4,
        },
      },
    },
  });

  it('respects requested videotype', () => {
    const vods = getVODsForChannel(state, {
      channelName: 'channel',
      count: 1,
      videoType: VideoType.Highlight,
    });

    expect(vods.length).toEqual(1);
    expect(vods[0].videoType).toEqual(VideoType.Highlight);
  });

  it('respects requested sortTarget', () => {
    const vods = getVODsForChannel(state, {
      channelName: 'channel',
      count: 2,
      videoType: VideoType.Archive,
      sortTarget: VODSortTarget.Date,
    });
    expect(vods.length).toEqual(2);
    expect(vods[0].date).toBeGreaterThan(vods[1].date);
  });

  it('returns empty list if no broadcast type matches found', () => {
    const emptyState = createTestState({
      data: {
        vods: {
          vodDetails: {
            vod1,
            vod2,
          },
        },
      },
    });
    const vods = getVODsForChannel(emptyState, {
      channelName: 'channel',
      count: 1,
      videoType: VideoType.Highlight,
    });
    expect(vods.length).toEqual(0);
  });

  it('returns empty list if no channel matches found', () => {
    const vods = getVODsForChannel(state, {
      channelName: 'other channel',
      count: 1,
      videoType: VideoType.Highlight,
    });
    expect(vods.length).toEqual(0);
  });
});

describe('getClipsForChannel', () => {
  const clip1 = {
    channel: 'channel',
    viewCount: 0,
    date: new Date().valueOf() - 1,
  };

  const clip2 = {
    channel: 'channel',
    viewCount: 1,
    date: 0,
  };

  const clip3 = {
    channel: 'not channel',
    viewCount: 2,
    date: new Date().valueOf(),
  };

  it('properly filters, sorts, and slices', () => {
    const state = createTestState({
      data: {
        clips: {
          clipDetails: {
            clip1,
            clip2,
            clip3,
          },
        },
      },
    });

    expect(
      getClipsForChannel(state, {
        channelName: 'channel',
        count: 1,
        filterForLastWeek: false,
      }),
    ).toEqual([clip2]);

    expect(
      getClipsForChannel(state, {
        channelName: 'channel',
        count: 1,
        filterForLastWeek: true,
      }),
    ).toEqual([clip1]);
  });

  it('returns empty list if no matches found', () => {
    const state = createTestState({
      data: {
        clips: {
          clipDetails: {
            clip1,
            clip2,
            clip3,
          },
        },
      },
    });
    expect(
      getClipsForChannel(state, {
        channelName: 'BigAndy',
        count: 1,
        filterForLastWeek: false,
      }),
    ).toEqual([]);
  });
});

describe('showOpenInApp', () => {
  Enum.values(Location)
    .filter(location => location !== Location.EventDetails)
    .forEach(location => {
      Enum.values(OS)
        .filter(os => ![OS.Microsoft, OS.Other, OS.Unknown].includes(os))
        .forEach(os => {
          it(`shows open in app for ${location} and ${os}`, () => {
            const state = createTestState({
              app: {
                location,
              },
              device: {
                browser: Browser.Silk,
                os,
              },
            });

            expect(showOpenInApp(state)).toBe(true);
          });
        });
    });

  it('does not show open in app if on event details page', () => {
    const state = createTestState({
      app: {
        location: Location.EventDetails,
      },
      device: {
        os: OS.iOS,
        browser: Browser.Safari,
      },
    });

    expect(showOpenInApp(state)).toBe(false);
  });

  [OS.Microsoft, OS.Other, OS.Unknown].forEach(os => {
    it(`does not show open in app if the OS is ${os}`, () => {
      const state = createTestState({
        app: {
          location: Location.Channel,
        },
        device: {
          browser: Browser.Chrome,
          os,
        },
      });
      expect(showOpenInApp(state)).toBe(false);
    });
  });
});
