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

goog.module('goog.cssomTest');
goog.setTestOnly();

const CssRuleType = goog.require('goog.cssom.CssRuleType');
const DomHelper = goog.require('goog.dom.DomHelper');
const cssom = goog.require('goog.cssom');
const testSuite = goog.require('goog.testing.testSuite');
const userAgent = goog.require('goog.userAgent');
const {assertIsHtmlIFrameElement} = goog.require('goog.asserts.dom');
const {getStyleNonce} = goog.require('goog.dom.safe');

// Since sheet cssom_test1.css's first line is to import
// cssom_test2.css, we should get 2 before one in the string.
let cssText = '.css-link-1 { display: block; } ' +
    '.css-import-2 { display: block; } ' +
    '.css-import-1 { display: block; } ' +
    '.css-style-1 { display: block; } ' +
    '.css-style-2 { display: block; } ' +
    '.css-style-3 { display: block; }';

const replacementCssText = '.css-repl-1 { display: block; }';

// We're going to toLowerCase cssText before testing, because IE returns
// CSS property names in UPPERCASE, and the function shouldn't
// "fix" the text as it would be expensive and rarely of use.
// Same goes for the trailing whitespace in IE.
// Same goes for fixing the optimized removal of trailing ; in rules.
// Also needed for Opera.
function fixCssTextForIe(cssText) {
  cssText = cssText.toLowerCase().replace(/\s*$/, '');
  if (cssText.match(/[^;] \}/)) {
    cssText = cssText.replace(/([^;]) \}/g, '$1; }');
  }
  return cssText;
}

// Tests the scenario where we have a known stylesheet and index.

testSuite({
  testGetFileNameFromStyleSheet() {
    // cast to create mock object.
    let styleSheet =
        /** @type {?} */ ({'href': 'http://foo.com/something/filename.css'});
    assertEquals('filename.css', cssom.getFileNameFromStyleSheet(styleSheet));

    styleSheet = /** @type {?} */ (
        {'href': 'https://foo.com:123/something/filename.css'});
    assertEquals('filename.css', cssom.getFileNameFromStyleSheet(styleSheet));

    styleSheet = /** @type {?} */ (
        {'href': 'http://foo.com/something/filename.css?bar=bas'});
    assertEquals('filename.css', cssom.getFileNameFromStyleSheet(styleSheet));

    styleSheet = /** @type {?} */ ({'href': 'filename.css?bar=bas'});
    assertEquals('filename.css', cssom.getFileNameFromStyleSheet(styleSheet));

    styleSheet = /** @type {?} */ ({'href': 'filename.css'});
    assertEquals('filename.css', cssom.getFileNameFromStyleSheet(styleSheet));
  },

  testGetAllCssStyleSheets() {
    // NOTE: getAllCssStyleSheets return type is wrong, it should be
    // !Array<!Stylesheet> rather than nullable array entries
    const styleSheets = /** @type {?} */ (cssom.getAllCssStyleSheets());
    assertEquals(4, styleSheets.length);
    // Makes sure they're in the right cascade order.
    assertEquals(
        'cssom_test_link_1.css',
        cssom.getFileNameFromStyleSheet(styleSheets[0]));
    assertEquals(
        'cssom_test_import_2.css',
        cssom.getFileNameFromStyleSheet(styleSheets[1]));
    assertEquals(
        'cssom_test_import_1.css',
        cssom.getFileNameFromStyleSheet(styleSheets[2]));
    // Not an external styleSheet
    assertNull(cssom.getFileNameFromStyleSheet(styleSheets[3]));
  },

  testGetAllCssText() {
    const allCssText = cssom.getAllCssText();
    assertEquals(cssText, fixCssTextForIe(allCssText));
  },

  testGetAllCssStyleRules() {
    const allCssRules = cssom.getAllCssStyleRules();
    assertEquals(6, allCssRules.length);
  },

  testAddCssText() {
    const newCssText = '.css-add-1 { display: block; }';
    const newCssNode = cssom.addCssText(newCssText);

    assertEquals(document.styleSheets.length, 3);

    const allCssText = cssom.getAllCssText();

    assertEquals(`${cssText} ${newCssText}`, fixCssTextForIe(allCssText));

    let cssRules = cssom.getAllCssStyleRules();
    assertEquals(7, cssRules.length);

    // Remove the new stylesheet now so it doesn't interfere with other
    // tests.
    newCssNode.parentNode.removeChild(newCssNode);
    // Sanity check.
    cssRules = cssom.getAllCssStyleRules();
    assertEquals(6, cssRules.length);
  },

  testAddCssTextUsesIframeNonce() {
    const iframeWindow =
        assertIsHtmlIFrameElement(document.getElementById('frame'))
            .contentWindow;
    assert(iframeWindow.document !== document);

    const newCssNode = cssom.addCssText(
        '.css-add-1 { display: block; }', new DomHelper(iframeWindow.document));
    // Cannot assert on a string literal because IE11 doesn't support nonces
    // whatsoever. getStyleNonce returns an empty string if nonce isn't present.
    assertEquals(
        getStyleNonce(iframeWindow),
        newCssNode['nonce'] || newCssNode.getAttribute('nonce') || '');
  },

  /** @suppress {missingProperties} cssRules not defined on StyleSheet */
  testAddCssRule() {
    // test that addCssRule correctly adds the rule to the style
    // sheet.
    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    const newCssRule = '.css-addCssRule { display: block; }';
    let rules = styleSheet.rules || styleSheet.cssRules;
    const origNumberOfRules = rules.length;

    cssom.addCssRule(styleSheet, newCssRule, 1);

    rules = styleSheet.rules || styleSheet.cssRules;
    const newNumberOfRules = rules.length;
    assertEquals(newNumberOfRules, origNumberOfRules + 1);

    // Remove the added rule so we don't mess up other tests.
    cssom.removeCssRule(styleSheet, 1);
  },

  /** @suppress {missingProperties} cssRules not defined on StyleSheet */
  testAddCssRuleAtPos() {
    // test that addCssRule correctly adds the rule to the style
    // sheet at the specified position.
    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    const newCssRule = '.css-addCssRulePos { display: block; }';
    let rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const origNumberOfRules = rules.length;

    // Firefox croaks if we try to insert a CSSRule at an index that
    // contains a CSSImport Rule. Since we deal only with CSSStyleRule
    // objects, we find the first CSSStyleRule and return its index.
    //
    // NOTE(user): We could have unified the code block below for all
    // browsers but IE6 horribly mangled up the stylesheet by creating
    // duplicate instances of a rule when removeCssRule was invoked
    // just after addCssRule with the looping construct in. This is
    // perfectly fine since IE's styleSheet.rules does not contain
    // references to anything but CSSStyleRules.
    let pos = 0;
    if (styleSheet.cssRules) {
      pos = Array.prototype.findIndex.call(
          rules, rule => rule.type == CssRuleType.STYLE);
    }
    cssom.addCssRule(styleSheet, newCssRule, pos);

    rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const newNumberOfRules = rules.length;
    assertEquals(newNumberOfRules, origNumberOfRules + 1);

    // Remove the added rule so we don't mess up other tests.
    cssom.removeCssRule(styleSheet, pos);

    rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    assertEquals(origNumberOfRules, rules.length);
  },

  testAddCssRuleNoIndex() {
    // How well do we handle cases where the optional index is
    //  not passed in?
    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    let rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const origNumberOfRules = rules.length;
    const newCssRule = '.css-addCssRuleNoIndex { display: block; }';

    // Try inserting the rule without specifying an index.
    // Make sure we don't throw an exception, and that we added
    // the entry.
    cssom.addCssRule(styleSheet, newCssRule);

    rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const newNumberOfRules = rules.length;
    assertEquals(newNumberOfRules, origNumberOfRules + 1);

    // Remove the added rule so we don't mess up the other tests.
    cssom.removeCssRule(styleSheet, newNumberOfRules - 1);

    rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    assertEquals(origNumberOfRules, rules.length);
  },

  testGetParentStyleSheetAfterGetAllCssStyleRules() {
    const cssRules = cssom.getAllCssStyleRules();
    const cssRule = cssRules[4];
    const parentStyleSheet = cssom.getParentStyleSheet(cssRule);
    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    assertEquals(styleSheet, parentStyleSheet);
  },

  /** @suppress {missingProperties} cssRules not defined on StyleSheet */
  testGetCssRuleIndexInParentStyleSheetAfterGetAllCssStyleRules() {
    const cssRules = cssom.getAllCssStyleRules();
    const cssRule = cssRules[4];
    // Note here that this is correct - IE's styleSheet.rules does not
    // contain references to anything but CSSStyleRules while FF and others
    // include anything that inherits from the CSSRule interface.
    // See http://dev.w3.org/csswg/cssom/#cssrule.
    const parentStyleSheet = cssom.getParentStyleSheet(cssRule);
    const ruleIndex = (parentStyleSheet.cssRules != null) ? 2 : 1;
    assertEquals(ruleIndex, cssom.getCssRuleIndexInParentStyleSheet(cssRule));
  },

  /** @suppress {missingProperties} cssRules not defined on StyleSheet */
  testGetCssRuleIndexInParentStyleSheetNonStyleRule() {
    // IE's styleSheet.rules only contain CSSStyleRules.
    if (!userAgent.IE) {
      const styleSheets = cssom.getAllCssStyleSheets();
      const styleSheet = styleSheets[3];
      const newCssRule = '@media print { .css-nonStyle { display: block; } }';
      cssom.addCssRule(styleSheet, newCssRule);
      const rules = styleSheet.rules || styleSheet.cssRules;
      const cssRule = rules[rules.length - 1];
      assertEquals(CssRuleType.MEDIA, cssRule.type);
      // Make sure we don't throw an exception.
      cssom.getCssRuleIndexInParentStyleSheet(cssRule, styleSheet);
      // Remove the added rule.
      cssom.removeCssRule(styleSheet, rules.length - 1);
    }
  },

  testReplaceCssRuleWithStyleSheetAndIndex() {
    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    const rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const index = 2;
    const origCssRule = rules[index];
    const origCssText =
        fixCssTextForIe(cssom.getCssTextFromCssRule(origCssRule));

    cssom.replaceCssRule(origCssRule, replacementCssText, styleSheet, index);

    const newRules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const newCssRule = newRules[index];
    const newCssText = cssom.getCssTextFromCssRule(newCssRule);
    assertEquals(replacementCssText, fixCssTextForIe(newCssText));

    // Now we need to re-replace our rule, to preserve parity for the other
    // tests.
    cssom.replaceCssRule(newCssRule, origCssText, styleSheet, index);
    const nowRules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const nowCssRule = nowRules[index];
    const nowCssText = cssom.getCssTextFromCssRule(nowCssRule);
    assertEquals(origCssText, fixCssTextForIe(nowCssText));
  },

  /** @suppress {missingProperties} cssRules not defined on StyleSheet */
  testReplaceCssRuleUsingGetAllCssStyleRules() {
    const cssRules = cssom.getAllCssStyleRules();
    const origCssRule = cssRules[4];
    const origCssText =
        fixCssTextForIe(cssom.getCssTextFromCssRule(origCssRule));
    // notice we don't pass in the stylesheet or index.
    cssom.replaceCssRule(origCssRule, replacementCssText);

    const styleSheets = cssom.getAllCssStyleSheets();
    const styleSheet = styleSheets[3];
    const rules = cssom.getCssRulesFromStyleSheet(styleSheet);
    const index = (styleSheet.cssRules != null) ? 2 : 1;
    const cssRule = rules[index];
    const cssText = fixCssTextForIe(cssom.getCssTextFromCssRule(cssRule));
    assertEquals(replacementCssText, cssText);

    // try getting it the other way around too.
    const newCssRules = cssom.getAllCssStyleRules();
    const newCssRule = newCssRules[4];
    const newCssText = fixCssTextForIe(cssom.getCssTextFromCssRule(newCssRule));
    assertEquals(replacementCssText, newCssText);

    // Now we need to re-replace our rule, to preserve parity for the other
    // tests.
    cssom.replaceCssRule(newCssRule, origCssText);
    const nowCssRules = cssom.getAllCssStyleRules();
    const nowCssRule = nowCssRules[4];
    const nowCssText = fixCssTextForIe(cssom.getCssTextFromCssRule(nowCssRule));
    assertEquals(origCssText, nowCssText);
  },
});
