/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview
 * @suppress {missingRequire} Overriding goog.i18n.DateTimeSymbols
 */

goog.module('goog.ui.DatePickerTest');
goog.setTestOnly();

const DateDate = goog.require('goog.date.Date');
const DatePicker = goog.require('goog.ui.DatePicker');
const DateRange = goog.require('goog.date.DateRange');
/** @suppress {extraRequire} */
const DateTimeSymbols = goog.require('goog.i18n.DateTimeSymbols');
const DateTimeSymbols_en_US = goog.require('goog.i18n.DateTimeSymbols_en_US');
const DateTimeSymbols_zh_HK = goog.require('goog.i18n.DateTimeSymbols_zh_HK');
const KeyCodes = goog.require('goog.events.KeyCodes');
const Role = goog.require('goog.a11y.aria.Role');
const TagName = goog.require('goog.dom.TagName');
const aria = goog.require('goog.a11y.aria');
const classlist = goog.require('goog.dom.classlist');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
const recordFunction = goog.require('goog.testing.recordFunction');
const style = goog.require('goog.style');
const testSuite = goog.require('goog.testing.testSuite');
const testingEvents = goog.require('goog.testing.events');

let picker;
const $$ = dom.getElementsByTagNameAndClass;
let sandbox;

testSuite({
  setUpPage() {
    sandbox = dom.getElement('sandbox');

    // Set the current date to a constant.
    Date.now = () => +new Date(2017, 9, 17);
  },

  tearDown() {
    picker.dispose();
    dom.removeChildren(sandbox);
  },

  testIsMonthOnLeft() {
    goog.i18n.DateTimeSymbols = DateTimeSymbols_en_US;
    picker = new DatePicker();
    picker.create(sandbox);
    const head = $$('tr', 'goog-date-picker-head')[0];
    /** @suppress {checkTypes} suppression added to enable type checking */
    const month = $$('button', 'goog-date-picker-month', head.firstChild)[0];
    assertSameElements(
        'Button element must have expected class names',
        ['goog-date-picker-btn', 'goog-date-picker-month'],
        classlist.get(month));
  },

  testIsYearOnLeft() {
    goog.i18n.DateTimeSymbols = DateTimeSymbols_zh_HK;
    picker = new DatePicker();
    picker.create(sandbox);
    const head = $$('tr', 'goog-date-picker-head')[0];
    /** @suppress {checkTypes} suppression added to enable type checking */
    const year = $$('button', 'goog-date-picker-year', head.firstChild)[0];
    assertSameElements(
        'Button element must have expected class names',
        ['goog-date-picker-btn', 'goog-date-picker-year'], classlist.get(year));
  },

  testHidingOfTableFoot0() {
    picker = new DatePicker();
    picker.setAllowNone(false);
    picker.setShowToday(false);
    picker.create(sandbox);
    const tFoot = $$('tfoot')[0];
    assertFalse(style.isElementShown(tFoot));
  },

  testHidingOfTableFoot1() {
    picker = new DatePicker();
    picker.setAllowNone(false);
    picker.setShowToday(true);
    picker.create(sandbox);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testHidingOfTableFoot2() {
    picker = new DatePicker();
    picker.setAllowNone(true);
    picker.setShowToday(false);
    picker.create(sandbox);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testHidingOfTableFoot3() {
    picker = new DatePicker();
    picker.setAllowNone(true);
    picker.setShowToday(true);
    picker.create(sandbox);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testHidingOfTableFootAfterCreate0() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setAllowNone(false);
    picker.setShowToday(false);
    const tFoot = $$('tfoot')[0];
    assertFalse(style.isElementShown(tFoot));
  },

  testHidingOfTableFootAfterCreate1() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setAllowNone(false);
    picker.setShowToday(true);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testHidingOfTableFootAfterCreate2() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setAllowNone(true);
    picker.setShowToday(false);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testHidingOfTableFootAfterCreate3() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setAllowNone(true);
    picker.setShowToday(true);
    const tFoot = $$('tfoot')[0];
    assertTrue(style.isElementShown(tFoot));
  },

  testLongDateFormat() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setLongDateFormat(true);
    const dates = $$('td', 'goog-date-picker-date');
    for (let i = 0; i < dates.length; i++) {
      assertEquals(2, dom.getTextContent(dates[i]).length);
    }
  },

  testGetActiveMonth() {
    picker = new DatePicker(new Date(2000, 5, 5));
    const month = picker.getActiveMonth();
    assertObjectEquals(new DateDate(2000, 5, 1), month);

    month.setMonth(10);
    assertObjectEquals(
        'modifying the returned object is safe', new DateDate(2000, 5, 1),
        picker.getActiveMonth());
  },

  testGetActiveMonthBeforeYear100() {
    const date = new Date(23, 5, 5);
    // Above statement will create date with year 1923, need to set full year
    // explicitly.
    date.setFullYear(23);

    const expectedMonth = new DateDate(23, 5, 1);
    expectedMonth.setFullYear(23);

    picker = new DatePicker(date);
    const month = picker.getActiveMonth();
    assertObjectEquals(expectedMonth, month);

    month.setMonth(10);
    assertObjectEquals(
        'modifying the returned object is safe', expectedMonth,
        picker.getActiveMonth());
  },

  testGetDate() {
    picker = new DatePicker(new Date(2000, 0, 1));
    const date = picker.getDate();
    assertObjectEquals(new DateDate(2000, 0, 1), date);

    date.setMonth(1);
    assertObjectEquals(
        'modifying the returned date is safe', new DateDate(2000, 0, 1),
        picker.getDate());

    picker.setDate(null);
    assertNull('no date is selected', picker.getDate());
  },

  testGetDateBeforeYear100() {
    const inputDate = new Date(23, 5, 5);
    // Above statement will create date with year 1923, need to set full year
    // explicitly.
    inputDate.setFullYear(23);
    picker = new DatePicker(inputDate);
    const date = picker.getDate();

    const expectedDate = new DateDate(23, 5, 5);
    expectedDate.setFullYear(23);
    assertObjectEquals(expectedDate, date);

    picker.setDate(inputDate);
    assertObjectEquals(expectedDate, picker.getDate());
    const expectedMonth = new DateDate(23, 5, 1);
    expectedMonth.setFullYear(23);
    assertObjectEquals(expectedMonth, picker.getActiveMonth());
  },

  testGridForDecember23() {
    // Initialize picker to December 23.
    const inputDate = new Date(23, 11, 5);
    // Above statement will create date with year 1923, need to set full year
    // explicitly.
    inputDate.setFullYear(23);
    picker = new DatePicker(inputDate);
    picker.create(sandbox);

    // Grid start with last days of November 23, shows December 23 and first
    // days of January 24.
    for (let i = 0; i < 6; i++) {
      for (let j = 0; j < 7; j++) {
        const date = picker.getDateAt(i, j);
        if (date.getMonth() == 0) {
          assertEquals(24, date.getFullYear());
        } else {
          assertEquals(23, date.getFullYear());
        }
      }
    }
  },

  testGridForJanuary22() {
    // Initialize picker to January 22.
    const inputDate = new Date(22, 0, 5);
    // Above statement will create date with year 1922, need to set full year
    // explicitly.
    inputDate.setFullYear(22);
    picker = new DatePicker(inputDate);
    picker.create(sandbox);

    // Grid start with last days of December 21, shows January 22 and first days
    // of February 22.
    for (let i = 0; i < 6; i++) {
      for (let j = 0; j < 7; j++) {
        const date = picker.getDateAt(i, j);
        if (date.getMonth() == 11) {
          assertEquals(21, date.getFullYear());
        } else {
          assertEquals(22, date.getFullYear());
        }
      }
    }
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testGetDateAt() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setDate(new Date(2000, 5, 5));
    const date = picker.getDateAt(0, 0);
    assertTrue(date.equals(picker.grid_[0][0]));

    date.setMonth(1);
    assertFalse(date.equals(picker.grid_[0][0]));
  },

  testGetDateAt_NotInGrid() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setDate(new Date(2000, 5, 5));
    let date = picker.getDateAt(-1, 0);
    assertNull(date);

    date = picker.getDateAt(0, -1);
    assertNull(date);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testGetDateElementAt() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setDate(new Date(2000, 5, 5));
    /** @suppress {visibility} suppression added to enable type checking */
    const element = picker.getDateElementAt(0, 0);
    assertEquals('td', element.tagName.toLowerCase());
    assertObjectEquals(element, picker.elTable_[1][1]);
  },

  testGetDateElementAt_NotInTable() {
    picker = new DatePicker();
    picker.create(sandbox);
    picker.setDate(new Date(2000, 5, 5));
    /** @suppress {visibility} suppression added to enable type checking */
    let element = picker.getDateElementAt(-1, 0);
    assertNull(element);

    /** @suppress {visibility} suppression added to enable type checking */
    element = picker.getDateElementAt(0, -1);
    assertNull(element);

    /** @suppress {visibility} suppression added to enable type checking */
    element = picker.getDateElementAt(picker.elTable_.length - 1, 0);
    assertNull(element);

    /** @suppress {visibility} suppression added to enable type checking */
    element = picker.getDateElementAt(0, picker.elTable_[0].length - 1);
    assertNull(element);
  },

  testSetDate() {
    picker = new DatePicker();
    picker.createDom();
    picker.enterDocument();
    let selectEvents = 0;
    let changeEvents = 0;
    let changeActiveMonthEvents = 0;
    events.listen(picker, DatePicker.Events.SELECT, () => {
      selectEvents++;
    });
    events.listen(picker, DatePicker.Events.CHANGE, () => {
      changeEvents++;
    });
    events.listen(picker, DatePicker.Events.CHANGE_ACTIVE_MONTH, () => {
      changeActiveMonthEvents++;
    });

    // Set date.
    picker.setDate(new Date(2010, 1, 26));
    assertEquals('no select event dispatched', 1, selectEvents);
    assertEquals('no change event dispatched', 1, changeEvents);
    assertEquals(
        'no change active month event dispatched', 1, changeActiveMonthEvents);
    assertTrue(
        'date is set', new DateDate(2010, 1, 26).equals(picker.getDate()));

    // Set date to same date.
    picker.setDate(new Date(2010, 1, 26));
    assertEquals('1 select event dispatched', 2, selectEvents);
    assertEquals('no change event dispatched', 1, changeEvents);
    assertEquals(
        'no change active month event dispatched', 1, changeActiveMonthEvents);

    // Set date to different date.
    picker.setDate(new Date(2010, 1, 27));
    assertEquals('another select event dispatched', 3, selectEvents);
    assertEquals('1 change event dispatched', 2, changeEvents);
    assertEquals(
        '2 change active month events dispatched', 1, changeActiveMonthEvents);

    // Set date to a date in a different month.
    picker.setDate(new Date(2010, 2, 27));
    assertEquals('another select event dispatched', 4, selectEvents);
    assertEquals('another change event dispatched', 3, changeEvents);
    assertEquals(
        '3 change active month event dispatched', 2, changeActiveMonthEvents);

    // Set date to none.
    picker.setDate(null);
    assertEquals('another select event dispatched', 5, selectEvents);
    assertEquals('another change event dispatched', 4, changeEvents);
    assertNull('date cleared', picker.getDate());
  },

  testChangeActiveMonth() {
    picker = new DatePicker();
    let changeActiveMonthEvents = 0;
    events.listen(picker, DatePicker.Events.CHANGE_ACTIVE_MONTH, () => {
      changeActiveMonthEvents++;
    });

    // Set date.
    picker.setDate(new Date(2010, 1, 26));
    assertEquals(
        'change active month events dispatched', 1, changeActiveMonthEvents);
    assertTrue(
        'date is set', new DateDate(2010, 1, 26).equals(picker.getDate()));

    // Change to next month.
    picker.nextMonth();
    assertEquals(
        '1 change active month event dispatched', 2, changeActiveMonthEvents);
    assertTrue(
        'date should still be the same',
        new DateDate(2010, 1, 26).equals(picker.getDate()));

    // Change to next year.
    picker.nextYear();
    assertEquals(
        '2 change active month events dispatched', 3, changeActiveMonthEvents);

    // Change to previous month.
    picker.previousMonth();
    assertEquals(
        '3 change active month events dispatched', 4, changeActiveMonthEvents);

    // Change to previous year.
    picker.previousYear();
    assertEquals(
        '4 change active month events dispatched', 5, changeActiveMonthEvents);
  },

  testChangeActiveMonth_whenGridGrows_dispatchesGridIncreaseEvent() {
    picker = new DatePicker();
    picker.setShowFixedNumWeeks(false);
    picker.render();
    let gridSizeIncreaseEvents = 0;
    events.listen(
        picker, DatePicker.Events.GRID_SIZE_INCREASE,
        () => void gridSizeIncreaseEvents++);

    // Feb 2015 has only four rows of dates.
    picker.setDate(new Date(2015, 1, 1));
    assertEquals('No grid size changes yet', 0, gridSizeIncreaseEvents);

    // Mar 2015 has 5 rows.
    picker.nextMonth();
    assertEquals(
        '1 grid size increase events dispatched', 1, gridSizeIncreaseEvents);

    // Going back to Feb is a size decrease, so no dispatch.
    picker.previousMonth();
    assertEquals(
        '1 grid size increase events dispatched', 1, gridSizeIncreaseEvents);

    // Going forward to Mar again should dispatch again.
    picker.nextMonth();
    assertEquals(
        '2 grid size increase events dispatched', 2, gridSizeIncreaseEvents);

    // Apr 2015 has 5 rows also.
    picker.nextMonth();
    assertEquals(
        '2 grid size increase events dispatched', 2, gridSizeIncreaseEvents);

    // May 2015 has 6 rows.
    picker.nextMonth();
    assertEquals(
        '3 grid size increase events dispatched', 3, gridSizeIncreaseEvents);
  },

  testChangeActiveMonth_withFixedNumWeeks_dispatchesNoGridIncreaseEvent() {
    picker = new DatePicker();
    picker.setShowFixedNumWeeks(true);
    picker.render();
    let gridSizeIncreaseEvents = 0;
    events.listen(
        picker, DatePicker.Events.GRID_SIZE_INCREASE,
        () => void gridSizeIncreaseEvents++);

    // Feb 2015 has only four rows of dates.
    picker.setDate(new Date(2015, 1, 1));
    assertEquals('No grid size changes yet', 0, gridSizeIncreaseEvents);

    for (let i = 0; i < 100; i++) {
      picker.nextMonth();
    }
    assertEquals('No grid size changes', 0, gridSizeIncreaseEvents);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testUserSelectableDates() {
    const dateRange =
        new DateRange(new DateDate(2010, 1, 25), new DateDate(2010, 1, 27));
    picker = new DatePicker();
    picker.setUserSelectableDateRange(dateRange);
    assertFalse(
        'should not be selectable date',
        picker.isUserSelectableDate_(new DateDate(2010, 1, 24)));
    assertTrue(
        'should be a selectable date',
        picker.isUserSelectableDate_(new DateDate(2010, 1, 25)));
    assertTrue(
        'should be a selectable date',
        picker.isUserSelectableDate_(new DateDate(2010, 1, 26)));
    assertTrue(
        'should be a selectable date',
        picker.isUserSelectableDate_(new DateDate(2010, 1, 27)));
    assertFalse(
        'should not be selectable date',
        picker.isUserSelectableDate_(new DateDate(2010, 1, 28)));
  },

  testGetUserSelectableDateRange() {
    picker = new DatePicker();
    let dateRange = picker.getUserSelectableDateRange();
    assertTrue(
        'default date range is all time',
        DateRange.equals(dateRange, DateRange.allTime()));
    const newDateRange =
        new DateRange(new DateDate(2010, 1, 25), new DateDate(2010, 1, 27));
    picker.setUserSelectableDateRange(newDateRange);
    dateRange = picker.getUserSelectableDateRange();
    assertTrue(
        'should be equal to updated date range',
        DateRange.equals(dateRange, newDateRange));
  },

  testUniqueCellIds() {
    picker = new DatePicker();
    picker.render();
    const cells = dom.getElementsByTagNameAndClass(
        TagName.TD, undefined, picker.getElement());
    const existingIds = {};
    const numCells = cells.length;
    for (let i = 0; i < numCells; i++) {
      assertNotNull(cells[i]);
      if (aria.getRole(cells[i]) == Role.GRIDCELL) {
        assertNonEmptyString('cell id is non empty', cells[i].id);
        assertUndefined('cell id is not unique', existingIds[cells[i].id]);
        existingIds[cells[i].id] = 1;
      }
    }
  },

  testDecoratePreservesClasses() {
    picker = new DatePicker();
    const div = dom.createDom(TagName.DIV, 'existing-class');
    picker.decorate(div);
    assertTrue(classlist.contains(div, picker.getBaseCssClass()));
    assertTrue(classlist.contains(div, 'existing-class'));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testKeyboardNavigation_arrowKey() {
    // This is a Sunday, so it's the first cell in the grid.
    picker = new DatePicker(new Date(2017, 9, 1));
    // Make the first column be Sunday, not week numbers
    picker.setShowWeekNum(false);
    picker.render(dom.getElement('sandbox'));
    const selectEvents = recordFunction();
    const changeEvents = recordFunction();
    events.listen(picker, DatePicker.Events.SELECT, selectEvents);
    events.listen(picker, DatePicker.Events.CHANGE, changeEvents);

    testingEvents.fireNonAsciiKeySequence(
        picker.getElement(), KeyCodes.DOWN, KeyCodes.DOWN);
    changeEvents.assertCallCount(1);
    selectEvents.assertCallCount(0);

    // Make sure the new selection is focused, for a11y.  elTable_[0] has the
    // week day headers, so +1 starts at the first row of days.
    assertEquals(picker.elTable_[1 + 1][1], document.activeElement);

    testingEvents.fireNonAsciiKeySequence(
        picker.getElement(), KeyCodes.ENTER, KeyCodes.ENTER);
    changeEvents.assertCallCount(1);
    selectEvents.assertCallCount(1);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testKeyboardNavigation_homeKey() {
    // This is a Sunday, so it's the first cell in the grid.
    picker = new DatePicker(new Date(2017, 9, 1));
    // Make the first column be Sunday, not week numbers
    picker.setShowWeekNum(false);
    picker.render(dom.getElement('sandbox'));
    const selectEvents = recordFunction();
    const changeEvents = recordFunction();
    events.listen(picker, DatePicker.Events.SELECT, selectEvents);
    events.listen(picker, DatePicker.Events.CHANGE, changeEvents);

    testingEvents.fireNonAsciiKeySequence(
        picker.getElement(), KeyCodes.HOME, KeyCodes.HOME);
    changeEvents.assertCallCount(1);
    selectEvents.assertCallCount(1);

    // Make sure the new selection is focused, for a11y.  elTable_[0] has the
    // week day headers, so +1 starts at the first row of days.
    assertEquals(picker.elTable_[2 + 1][3], document.activeElement);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testKeyboardNavigationOnUnselectableRange_homeKey() {
    // This is a Sunday, so it's the first cell in the grid.
    picker = new DatePicker(new Date(2017, 9, 1));
    // Make the first column be Sunday, not week numbers
    picker.setShowWeekNum(false);
    picker.render(dom.getElement('sandbox'));
    const selectEvents = recordFunction();
    const changeEvents = recordFunction();
    events.listen(picker, DatePicker.Events.SELECT, selectEvents);
    events.listen(picker, DatePicker.Events.CHANGE, changeEvents);
    picker.setUserSelectableDateRange(
        new DateRange(new DateDate(200, 1, 1), new DateDate(300, 1, 1)));

    testingEvents.fireNonAsciiKeySequence(
        picker.getElement(), KeyCodes.HOME, KeyCodes.HOME);

    changeEvents.assertCallCount(0);
    selectEvents.assertCallCount(0);
  },

  testDayGridHasNonEmptyAriaLabels() {
    picker = new DatePicker(new Date(2017, 8, 9));
    picker.render(dom.getElement('sandbox'));

    const cells = dom.getElementsByTagNameAndClass(
        TagName.TD, undefined, picker.getElement());
    const numCells = cells.length;
    for (let i = 0; i < numCells; i++) {
      assertNotNull(cells[i]);
      if (aria.getRole(cells[i]) == Role.GRIDCELL) {
        assertNonEmptyString(
            'Aria label in date cell should not be empty',
            aria.getLabel(cells[i]));
      }
    }
  },
});
