/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

describe('Localization', () => {
  const Localization = shaka.ui.Localization;

  // https://bit.ly/2PbzxlD
  const DWARFISH = 'dwarfish';
  const DWARFISH_NORTH = 'dwarfish-NORTH';
  const DWARFISH_SOUTH = 'dwarfish-SOUTH';

  const ELVISH = 'elvish';
  const ELVISH_WOODLAND = 'elvish-WOODLAND';

  const HALFLING = 'halfling';
  const HALFLING_COMMON = 'halfling-COMMON';
  const HALFLING_RIVER = 'halfling-RIVER';
  const HALFLING_SHIRE = 'halfling-SHIRE';

  describe('insert', () => {
    const EN_US = 'en-US';

    it('can add new data', () => {
      /** @type {!shaka.ui.Localization} */
      const localization = new Localization(EN_US);
      localization.insert(EN_US, new Map([
        ['hello', 'howdy'],
        ['good-bye', 'cheerio'],
      ]));

      expect(localization.resolve('hello')).toBe('howdy');
      expect(localization.resolve('good-bye')).toBe('cheerio');
    });

    it('can replace old data when adding new data', () => {
      /** @type {!shaka.ui.Localization} */
      const localization = new Localization(EN_US);
      localization.insert(EN_US, new Map([
        ['hello', 'howdy'],
        ['good-bye', 'cheerio'],
      ]));

      expect(localization.resolve('hello')).toBe('howdy');
      expect(localization.resolve('good-bye')).toBe('cheerio');

      localization.insert(EN_US, new Map([
        ['good-bye', 'farewell'],
        ['thank-you', 'thank-you'],
      ]));

      expect(localization.resolve('hello')).toBe('howdy');
      expect(localization.resolve('good-bye')).toBe('farewell');
      expect(localization.resolve('thank-you')).toBe('thank-you');
    });

    it('can keep old data when adding new', () => {
      const USE_OLD = Localization.ConflictResolution.USE_OLD;

      /** @type {!shaka.ui.Localization} */
      const localization = new Localization(EN_US);
      localization.insert(EN_US, new Map([
        ['hello', 'howdy'],
        ['good-bye', 'cheerio'],
      ]));

      expect(localization.resolve('hello')).toBe('howdy');
      expect(localization.resolve('good-bye')).toBe('cheerio');

      localization.insert(EN_US, new Map([
        ['hello', 'greetings'],
        ['good-bye', 'farewell'],
        ['thank-you', 'thank-you'],
      ]), USE_OLD);

      // Nothing should have changed.
      expect(localization.resolve('hello')).toBe('howdy');
      expect(localization.resolve('good-bye')).toBe('cheerio');
      expect(localization.resolve('thank-you')).toBe('thank-you');
    });
  });

  describe('locale fallback', () => {
    const KEY = 'KEY';

    const FALLBACK = 'FALLBACK';
    const FALLBACK_VALUE = 'FALLBACK VALUE';

    const ELVISH_VALUE = 'ELVISH VALUE';

    const HALFLING_COMMON_VALUE = 'HALFLING-COMMON VALUE';
    const HALFLING_SHIRE_VALUE = 'HALFLING-SHIRE VALUE';

    /** @type {!shaka.ui.Localization} */
    let localization;

    const insert = (locale, value) => {
      localization.insert(locale, new Map([[KEY, value]]));
    };

    const testLocales = (locales, expectedValue) => {
      localization.changeLocale(locales);
      const value = localization.resolve(KEY);
      expect(value).toBe(expectedValue);
    };

    beforeEach(() => {
      localization = new Localization(FALLBACK);

      // Insert translations to test various scenarios below:
      insert(FALLBACK, FALLBACK_VALUE);
      insert(ELVISH, ELVISH_VALUE);
      insert(HALFLING_COMMON, HALFLING_COMMON_VALUE);
      insert(HALFLING_SHIRE, HALFLING_SHIRE_VALUE);
    });

    it('resorts to final fallback for one unknown locale', () => {
      // For one locale we don't have translations for, go to the fallback.
      testLocales([DWARFISH_NORTH], FALLBACK_VALUE);
    });

    it('resorts to final fallback for multiple unknown locales', () => {
      // For multiple locales in which we don't have translations, go to the
      // fallback.
      testLocales([
        DWARFISH_NORTH,
        DWARFISH_SOUTH,
      ], FALLBACK_VALUE);
    });

    it('prefers a known locale from the list instead of final fallback', () => {
      // If we have a locale in the list for which we _do_ have translations,
      // expect to see the translated value.
      testLocales([
        DWARFISH_NORTH,
        DWARFISH_SOUTH,
        ELVISH,
      ], ELVISH_VALUE);
    });

    it('prefers the first known locale from the list', () => {
      // If we have multiple locales in the list for which we _do_ have
      // translations, expect to see the translated value from the first one.
      testLocales([
        DWARFISH_NORTH,
        DWARFISH_SOUTH,
        HALFLING_COMMON,
        ELVISH,
      ], HALFLING_COMMON_VALUE);
    });

    it('finds translations from the parent locale', () => {
      // If we have a locale in the list for which we have a region-less
      // translation, expect to see the translated value.
      testLocales([ELVISH_WOODLAND], ELVISH_VALUE);
    });

    it('finds translations from a locale with a different region', () => {
      // If there's a locale in the list for which we have translations in a
      // sibling locale (same language, different region), expect to see that
      // value.  Common is chosen over shire because equivalent-distance locales
      // are alphabetically sorted to break ties.
      testLocales([HALFLING_RIVER], HALFLING_COMMON_VALUE);
    });

    it('finds translations from a child locale', () => {
      // If there's a locale in the list for which we have translations in a
      // child locale (same language, but for a specific region), expect to see
      // that value.  Common is chosen over shire because equivalent-distance
      // locales are alphabetically sorted to break ties.
      testLocales([HALFLING], HALFLING_COMMON_VALUE);
    });

    it('prefers a relative of an earlier locale to an exact later one', () => {
      // If we have multiple locales in the list, and an earlier one has
      // relatives with translations, expect to see that chosen over an exact
      // match later in the list.
      testLocales([
        HALFLING,
        ELVISH,
      ], HALFLING_COMMON_VALUE);
    });
  });

  describe('unknown locales event', () => {
    it('fires when we change to a locale we have not loaded', () => {
      const events = [];

      const localization = new Localization(/* fallback= */ HALFLING_COMMON);
      collectEvents(localization, Localization.UNKNOWN_LOCALES, events);

      localization.insert(HALFLING_COMMON, new Map());
      localization.changeLocale([ELVISH_WOODLAND]);

      expect(events.length).toBe(1);
      expect(events[0].locales).toEqual([ELVISH_WOODLAND]);
    });


    it('will not fire after we add the locale', () => {
      const events = [];

      const localization = new Localization(/* fallback= */ HALFLING_COMMON);
      collectEvents(localization, Localization.UNKNOWN_LOCALES, events);

      // We should see an event telling us that both elvish and dwarfish are
      // missing.
      localization.insert(HALFLING_COMMON, new Map());
      localization.changeLocale([ELVISH_WOODLAND, DWARFISH_NORTH]);

      expect(events.length).toBe(1);
      expect(events[0].locales).toEqual([ELVISH_WOODLAND, DWARFISH_NORTH]);

      // We should see an event telling us that dwarfish is still missing.
      localization.insert(ELVISH_WOODLAND, new Map());
      localization.changeLocale([ELVISH_WOODLAND, DWARFISH_NORTH]);

      expect(events.length).toBe(2);
      expect(events[0].locales).toEqual([ELVISH_WOODLAND, DWARFISH_NORTH]);
      expect(events[1].locales).toEqual([DWARFISH_NORTH]);

      // There should be no more events now that we added the last language.
      localization.insert(DWARFISH_NORTH, new Map());
      localization.changeLocale([ELVISH_WOODLAND, DWARFISH_NORTH]);

      expect(events.length).toBe(2);
      expect(events[0].locales).toEqual([ELVISH_WOODLAND, DWARFISH_NORTH]);
      expect(events[1].locales).toEqual([DWARFISH_NORTH]);
    });
  });

  describe('unknown localization event', () => {
    const HOW_ARE_YOU = 'how are you';

    const HALFLING_COMMON_MAP = new Map().set(
        HOW_ARE_YOU, 'How ye this topa the mornin?');
    const HALFLING_SHIRE_MAP = new Map().set(
        HOW_ARE_YOU, 'How be the day treating ya?');
    const DWARFISH_MAP = new Map().set(
        HOW_ARE_YOU, 'You alive?');
    const DWARFISH_NORTH_MAP = new Map().set(
        HOW_ARE_YOU, 'Is the fire alive in ye soul?');
    const DWARFISH_SOUTH_MAP = new Map().set(
        HOW_ARE_YOU, 'You dead yet?');

    /** @type {!shaka.ui.Localization} */
    let localization;

    beforeEach(() => {
      localization = new shaka.ui.Localization(HALFLING_COMMON);

      // Insert every locale into the system but leave them empty. Each test
      // will add entries as they need them.
      const emptyMap = new Map();
      localization.insert(DWARFISH, emptyMap);
      localization.insert(DWARFISH_NORTH, emptyMap);
      localization.insert(DWARFISH_SOUTH, emptyMap);
      localization.insert(HALFLING_COMMON, emptyMap);
      localization.insert(HALFLING_SHIRE, emptyMap);
    });

    it('will not fire when key is found in preferred', () => {
      localization.insert(DWARFISH_NORTH, DWARFISH_NORTH_MAP);
      localization.changeLocale([DWARFISH_NORTH]);

      const events = [];
      collectEvents(localization, Localization.UNKNOWN_LOCALIZATION, events);

      // We expect to see a non-empty string returned and to see NO "missing
      // localization" events.
      expect(localization.resolve(HOW_ARE_YOU)).toBeTruthy();
      expect(events.length).toBe(0);
    });

    it('will not fire when key is found in base', () => {
      localization.insert(DWARFISH, DWARFISH_MAP);
      localization.changeLocale([DWARFISH_NORTH]);

      const events = [];
      collectEvents(localization, Localization.UNKNOWN_LOCALIZATION, events);

      // We expect to see a non-empty string returned and to see NO "missing
      // localization" events.
      expect(localization.resolve(HOW_ARE_YOU)).toBeTruthy();
      expect(events.length).toBe(0);
    });

    it('will not fire when key is found in sibling', () => {
      localization.insert(DWARFISH_SOUTH, DWARFISH_SOUTH_MAP);
      localization.changeLocale([DWARFISH_NORTH]);

      const events = [];
      collectEvents(localization, Localization.UNKNOWN_LOCALIZATION, events);

      // We expect to see a non-empty string returned and to see NO "missing
      // localization" events.
      expect(localization.resolve(HOW_ARE_YOU)).toBeTruthy();
      expect(events.length).toBe(0);
    });

    it('will not fire when key is found in fallback', () => {
      localization.insert(HALFLING_COMMON, HALFLING_COMMON_MAP);
      localization.changeLocale([DWARFISH_NORTH]);

      const events = [];
      collectEvents(localization, Localization.UNKNOWN_LOCALIZATION, events);

      // We expect to see a non-empty string returned and to see NO "missing
      // localization" events.
      expect(localization.resolve(HOW_ARE_YOU)).toBeTruthy();
      expect(events.length).toBe(0);
    });

    it('fires when key is not found', () => {
      localization.changeLocale([DWARFISH_NORTH]);

      const events = [];
      collectEvents(localization, Localization.UNKNOWN_LOCALIZATION, events);

      // When nothing is found an empty string should be returned and we should
      // see a "missing localization" event.
      expect(localization.resolve(HOW_ARE_YOU)).toBe('');
      expect(events.length).toBe(1);
    });

    // This test is similar to "missing from everything", but the difference
    // here is that the entry can be found in a locale that is not part of
    // search.
    it('fires when key is not found in fallback, siblings, base, or preferred',
        () => {
          localization.insert(HALFLING_SHIRE, HALFLING_SHIRE_MAP);
          localization.changeLocale([DWARFISH_NORTH]);

          const events = [];
          collectEvents(
              localization, Localization.UNKNOWN_LOCALIZATION, events);

          // When nothing is found an empty string should be returned and we
          // should see a "missing localization" event.
          expect(localization.resolve(HOW_ARE_YOU)).toBe('');
          expect(events.length).toBe(1);
        });
  });

  // The "missing localizations event" is fired when all preferred locales do
  // not have the requested localization but a related locale does have the
  // requested localization
  describe('missing localizations event', () => {
    it('fires when key/value is missing from preferred but found in fallback',
        () => {
          // Initialize the localization system so that we have an entry for
          // "hello" in our fallback, but not in our preferred locale.
          const localization = new Localization(HALFLING_COMMON);
          localization.insert(HALFLING_COMMON, new Map([
            ['hello', 'Hallo! Hallo! And who may you be?'],
          ]));
          localization.insert(ELVISH_WOODLAND, new Map());

          const events = [];
          collectEvents(
              localization, Localization.MISSING_LOCALIZATIONS, events);

          // Change locales should cause the missing localizations event to fire
          // and report that we are missing an entry for "hello".
          localization.changeLocale([ELVISH_WOODLAND]);

          // We should be told that we are missing an entry for "hello" because
          // it was in the fallback (HALFLING) but not our preferred language
          // (EVLISH).
          expect(events.length).toBe(1);
          expect(events[0].locales).toEqual([ELVISH_WOODLAND]);
          expect(events[0].missing).toEqual(['hello']);
        });

    it('fires when key/value is missing from preferred but found in base',
        () => {
          // Initialize the localization system so that we have an entry for
          // "Best Wishes" in our base language, but not in our preferred
          // locale.
          const localization = new Localization(HALFLING_COMMON);
          localization.insert(ELVISH_WOODLAND, new Map());
          localization.insert(ELVISH, new Map([
            ['Best Wishes', 'Merin sa haryalye alasse'],
          ]));

          const events = [];
          collectEvents(
              localization, Localization.MISSING_LOCALIZATIONS, events);

          // Change locales should cause the missing localizations event to fire
          // and report that we are missing an entry for "Best Wishes".
          localization.changeLocale([ELVISH_WOODLAND]);

          // We should be told that we are missing an entry for "Best Wishes"
          // because it was in the base (ELVISH) but not our preferred language
          // (EVLISH_WOODLAND).
          expect(events.length).toBe(1);
          expect(events[0].locales).toEqual([ELVISH_WOODLAND]);
          expect(events[0].missing).toEqual(['Best Wishes']);
        });

    it('fires when key/value is missing from preferred but found in sibling',
        () => {
          // Initialize the localization system so that we have an entry for
          // "Do you understand" in a language that shares a base language with
          // our preferred locale, but not in our preferred locale.
          const localization = new Localization(ELVISH_WOODLAND);
          localization.insert(HALFLING_COMMON, new Map());
          localization.insert(HALFLING_SHIRE, new Map([
            ['Do you understand', 'If you take my meaning.'],
          ]));

          const events = [];
          collectEvents(
              localization, Localization.MISSING_LOCALIZATIONS, events);

          // Change locales should cause the missing localizations event to fire
          // and report that we are missing an entry for "Best Wishes".
          localization.changeLocale([HALFLING_COMMON]);

          // We should be told that we are missing an entry for "Do you
          // understand" because it was in a sibling locale (HALFLING_SHIRE)
          // but not our preferred language (HALFLING_COMMON).
          expect(events.length).toBe(1);
          expect(events[0].locales).toEqual([HALFLING_COMMON]);
          expect(events[0].missing).toEqual(['Do you understand']);
        });

    it('fires when key/value is missing from some preferred languages',
        () => {
          // Initialize the localization system so that we have an entry for
          // "may your forge burn bright" in our second preference but not our
          // first.
          const localization = new Localization(HALFLING_COMMON);
          localization.insert(HALFLING_COMMON, new Map());
          localization.insert(ELVISH_WOODLAND, new Map());
          localization.insert(DWARFISH_NORTH, new Map([
            ['may your forge burn bright', 'tan menu selek lanun khun'],
          ]));

          const events = [];
          collectEvents(
              localization, Localization.MISSING_LOCALIZATIONS, events);

          // Changing locales should not fire the missing localization event
          // because the localization for "may your forge burn bright" can
          // be found in our secondary preferred language.
          localization.changeLocale([ELVISH_WOODLAND, DWARFISH_NORTH]);

          // There should have no missing localization events.
          expect(events.length).toBe(1);
          expect(events[0].locales).toEqual([ELVISH_WOODLAND, DWARFISH_NORTH]);
          expect(events[0].missing).toEqual(['may your forge burn bright']);
        });
  });

  describe('locale changed event', () => {
    it('fires when locale changes', () => {
      const localization = new Localization(HALFLING_COMMON);

      const events = [];
      collectEvents(localization, Localization.LOCALE_CHANGED, events);

      // We are going from nothing to something, we should see an event.
      localization.changeLocale([DWARFISH_SOUTH]);
      expect(events.length).toBe(1);

      // We are changing from SOUTH to NORTH, we should see another event.
      localization.changeLocale([DWARFISH_NORTH]);
      expect(events.length).toBe(2);
    });

    it('fires when changing to the same locale', () => {
      const localization = new Localization(HALFLING_COMMON);

      const events = [];
      collectEvents(localization, Localization.LOCALE_CHANGED, events);

      // We are going from nothing to something, we should see an event.
      localization.changeLocale([DWARFISH_SOUTH]);
      expect(events.length).toBe(1);

      // We are going to ask to go our current locale, even through
      // conceptually this would be a no-op, the event should still fire.
      localization.changeLocale([DWARFISH_SOUTH]);
      expect(events.length).toBe(2);
    });
  });

  /**
   * Listen to |localization| for specific events and save them in |out|.
   *
   * @param {!shaka.ui.Localization} localization
   * @param {string} eventName
   * @param {!Array.<!shaka.util.FakeEvent>} out
   */
  function collectEvents(localization, eventName, out) {
    const onEvent = (event) => {
      const fakeEvent = /** @type {!shaka.util.FakeEvent} */ (event);
      out.push(fakeEvent);
    };

    localization.addEventListener(eventName, onEvent);
  }
});
