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

/** @fileoverview Unit tests for SafeStyle and its builders. */

goog.module('goog.html.safeStyleTest');
goog.setTestOnly();

const Const = goog.require('goog.string.Const');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const SafeStyle = goog.require('goog.html.SafeStyle');
const SafeUrl = goog.require('goog.html.SafeUrl');
const googObject = goog.require('goog.object');
const testSuite = goog.require('goog.testing.testSuite');

/**
 * Asserts that created SafeStyle matches expected value.
 * @param {string} expected
 * @param {!SafeStyle.PropertyMap} style
 */
function assertCreateEquals(expected, style) {
  const styleWrapped = SafeStyle.create(style);
  assertEquals(expected, SafeStyle.unwrap(styleWrapped));
}

const stubs = new PropertyReplacer();

testSuite({
  tearDown() {
    stubs.reset();
  },

  testConstructor_throwsOnBadToken() {
    assertThrows(() => new (/** @type {?} */ (SafeStyle))(''));
    assertThrows(
        () => new (/** @type {?} */ (SafeStyle.EMPTY)).constructor(''));
  },

  testSafeStyle() {
    const style = 'width: 1em;height: 1em;';
    const safeStyle = SafeStyle.fromConstant(Const.from(style));
    const extracted = SafeStyle.unwrap(safeStyle);
    assertEquals(style, extracted);
    assertEquals(style, safeStyle.getTypedStringValue());
    assertEquals(`${style}`, String(safeStyle));

    // Interface marker is present.
    assertTrue(safeStyle.implementsGoogStringTypedString);
  },

  /** @suppress {checkTypes} */
  testUnwrap() {
    const privateFieldName = 'privateDoNotAccessOrElseSafeStyleWrappedValue_';
    const propNames =
        googObject.getKeys(SafeStyle.fromConstant(Const.from('')));
    assertContains(privateFieldName, propNames);
    const evil = {};
    evil[privateFieldName] = 'width: expression(evil);';

    const exception = assertThrows(() => {
      SafeStyle.unwrap(evil);
    });
    assertContains('expected object of type SafeStyle', exception.message);
  },

  testFromConstant_allowsEmptyString() {
    assertEquals(SafeStyle.EMPTY, SafeStyle.fromConstant(Const.from('')));
  },

  testFromConstant_throwsIfNoFinalSemicolon() {
    assertThrows(() => {
      SafeStyle.fromConstant(Const.from('width: 1em'));
    });
  },

  testFromConstant_throwsIfNoColon() {
    assertThrows(() => {
      SafeStyle.fromConstant(Const.from('width= 1em;'));
    });
  },

  testEmpty() {
    assertEquals('', SafeStyle.unwrap(SafeStyle.EMPTY));
  },

  testCreate() {
    assertCreateEquals(
        'background:url(i.png);margin:0;',
        {'background': Const.from('url(i.png)'), 'margin': '0'});
  },

  testCreate_allowsEmpty() {
    assertEquals(SafeStyle.EMPTY, SafeStyle.create({}));
  },

  testCreate_skipsNull() {
    const style = SafeStyle.create({'background': null});
    assertEquals(SafeStyle.EMPTY, style);
  },

  testCreate_allowsLengths() {
    assertCreateEquals(
        'padding:0 1px .2% 3.4em;',  // expected
        {'padding': '0 1px .2% 3.4em'});
  },

  testCreate_allowsRgb() {
    assertCreateEquals(
        'color:rgb(10,20,30);',  // expected
        {'color': 'rgb(10,20,30)'});
    assertCreateEquals(
        'color:rgb(10%, 20%, 30%);',  // expected
        {'color': 'rgb(10%, 20%, 30%)'});
    assertCreateEquals(
        'background:0 5px rgb(10,20,30);',  // expected
        {'background': '0 5px rgb(10,20,30)'});
    assertCreateEquals(
        'background:rgb(10,0,0), rgb(0,0,30);',
        {'background': 'rgb(10,0,0), rgb(0,0,30)'});
  },

  testCreate_allowsRgba() {
    assertCreateEquals(
        'color:rgba(10,20,30,0.1);',  // expected
        {'color': 'rgba(10,20,30,0.1)'});
    assertCreateEquals(
        'color:rgba(10%, 20%, 30%, .5);',  // expected
        {'color': 'rgba(10%, 20%, 30%, .5)'});
  },

  testCreate_allowsCalc() {
    assertCreateEquals(
        'height:calc(100% * 0.8 - 20px + 3vh);',  // expected
        {'height': 'calc(100% * 0.8 - 20px + 3vh)'});
  },

  testCreate_allowsRepeat() {
    assertCreateEquals(
        'grid-template-columns:repeat(3, [start] 100px [end]);',
        {'grid-template-columns': 'repeat(3, [start] 100px [end])'});
  },

  testCreate_allowsSteps() {
    assertCreateEquals(
        'animation-timing-function:steps(2, start);',
        {'animation-timing-function': 'steps(2, start)'});
  },

  testCreate_allowsCubicBezier() {
    assertCreateEquals(
        'transition-timing-function:cubic-bezier(0.26, 0.86, 0.44, 0.95);',
        {'transition-timing-function': 'cubic-bezier(0.26, 0.86, 0.44, 0.95)'});
  },

  testCreate_allowsMinmax() {
    assertCreateEquals(
        'grid-template-columns:minmax(max-content, 50px) 20px;',
        {'grid-template-columns': 'minmax(max-content, 50px) 20px'});
  },

  testCreate_allowsFitContent() {
    assertCreateEquals(
        'grid-template-columns:fit-content(50px) 20px;',
        {'grid-template-columns': 'fit-content(50px) 20px'});
  },

  testCreate_allowsScale() {
    assertCreateEquals(
        'transform:scale(.5, 2);',  // expected
        {'transform': 'scale(.5, 2)'});
  },

  testCreate_allowsRotate() {
    assertCreateEquals(
        'transform:rotate(45deg);',  // expected
        {'transform': 'rotate(45deg)'});
  },

  testCreate_allowsTranslate() {
    assertCreateEquals(
        'transform:translate(10px);',  // expected
        {'transform': 'translate(10px)'});
    assertCreateEquals(
        'transform:translateX(5px);',  // expected
        {'transform': 'translateX(5px)'});
  },

  testCreate_allowsVar() {
    assertCreateEquals(
        'color:var(--xyz);',  // expected
        {'color': 'var(--xyz)'});
    assertCreateEquals(
        'color:var(--xyz, black);',  // expected
        {'color': 'var(--xyz, black)'});
  },

  testCreate_allowsLinearGradient() {
    assertCreateEquals(
        'background:linear-gradient(red, blue);',
        {'background': 'linear-gradient(red, blue)'});
    assertCreateEquals(
        'background:linear-gradient(rgb(10,0,0), rgb(0,0,30));',
        {'background': 'linear-gradient(rgb(10,0,0), rgb(0,0,30))'});
    assertCreateEquals(
        'background:linear-gradient(#333, #eee);',
        {'background': 'linear-gradient(#333, #eee)'});
    assertCreateEquals(
        'background:linear-gradient(#C0C0C0, #FF0000);',
        {'background': 'linear-gradient(#C0C0C0, #FF0000)'});
  },

  testCreate_allowsRadialGradient() {
    assertCreateEquals(
        'background:radial-gradient(red, blue);',
        {'background': 'radial-gradient(red, blue)'});
    assertCreateEquals(
        'background:radial-gradient(rgb(10,0,0), rgb(0,0,30));',
        {'background': 'radial-gradient(rgb(10,0,0), rgb(0,0,30))'});
    assertCreateEquals(
        'background:radial-gradient(#333, #eee);',
        {'background': 'radial-gradient(#333, #eee)'});
    assertCreateEquals(
        'background:radial-gradient(#C0C0C0, #FF0000);',
        {'background': 'radial-gradient(#C0C0C0, #FF0000)'});
  },

  testCreate_allowsSafeUrl() {
    assertCreateEquals('background:url("http://example.com");', {
      'background': SafeUrl.fromConstant(Const.from('http://example.com')),
    });
  },

  testCreate_allowsSafeUrlWithSpecialCharacters() {
    assertCreateEquals('background:url("http://example.com/\\"");', {
      'background': SafeUrl.fromConstant(Const.from('http://example.com/"')),
    });
    assertCreateEquals('background:url("http://example.com/%3c");', {
      'background': SafeUrl.fromConstant(Const.from('http://example.com/<')),
    });
    assertCreateEquals('background:url("http://example.com/;");', {
      'background': SafeUrl.fromConstant(Const.from('http://example.com/;')),
    });
  },

  testCreate_allowsArray() {
    const url = SafeUrl.fromConstant(Const.from('http://example.com'));
    assertCreateEquals(
        'background:red url("http://example.com") repeat-y;',
        {'background': ['red', url, 'repeat-y']});
  },

  testCreate_allowsUrl() {
    assertCreateEquals(
        'background:url(http://example.com);',
        {'background': 'url(http://example.com)'});
    assertCreateEquals(
        'background:url("http://example.com");',
        {'background': 'url("http://example.com")'});
    assertCreateEquals(
        'background:url( \'http://example.com\' );',
        {'background': 'url( \'http://example.com\' )'});
    assertCreateEquals(
        'background:url(http://example.com) red;',
        {'background': 'url(http://example.com) red'});
    assertCreateEquals(
        'background:url(' + SafeUrl.INNOCUOUS_STRING + ');',
        {'background': 'url(javascript:alert)'});
    assertCreateEquals(
        'background:url(")");',  // Expected.
        {'background': 'url(")")'});
    assertCreateEquals(
        'background:url(" ");',  // Expected.
        {'background': 'url(" ")'});
    assertThrows(() => {
      SafeStyle.create({'background': 'url(\'http://example.com\'"")'});
    });
    assertThrows(() => {
      SafeStyle.create({'background': 'url("\\\\")'});
    });
    assertThrows(() => {
      SafeStyle.create({'background': 'url(a""b)'});
    });
  },

  testCreate_throwsOnForbiddenCharacters() {
    assertThrows(() => {
      SafeStyle.create({'<': '0'});
    });
  },

  testCreate_allowsNestedFunctions() {
    assertCreateEquals(
        'grid-template-columns:repeat(3, minmax(100px, 200px));',
        {'grid-template-columns': 'repeat(3, minmax(100px, 200px))'});
    assertThrows(() => {
      SafeStyle.create({
        'grid-template-columns':
            'repeat(3, minmax(100px, minmax(200px, 300px)))',
      });
    });
  },

  testCreate_disallowsComments() {
    assertThrows(() => {
      SafeStyle.create({'color': 'rgb(/*)'});
    });
  },

  testCreate_allowBalancedSquareBrackets() {
    assertCreateEquals(
        'grid-template-columns:[trackName] 20px [other_track-name];',
        {'grid-template-columns': '[trackName] 20px [other_track-name]'});
    assertThrows(() => {
      SafeStyle.create({'grid-template-columns': '20px ["trackName"]'});
    });
    assertThrows(() => {
      SafeStyle.create({'grid-template-columns': '20px [tra[ckName]'});
    });
    assertThrows(() => {
      SafeStyle.create({'grid-template-columns': '20px [tra'});
    });
    assertThrows(() => {
      SafeStyle.create({'grid-template-columns': '20px [tra ckName]'});
    });
    assertThrows(() => {
      SafeStyle.create({'grid-template-columns': '20px [trackName] 20px]'});
    });
  },

  testCreate_values() {
    const valids = [
      '0',
      '0 0',
      '1px',
      '100%',
      '2.3px',
      '.1em',
      'red',
      '#f00',
      'red !important',
      '"Times New Roman"',
      '\'Times New Roman\'',
      '"Bold \'nuff"',
      '"O\'Connor\'s Revenge"',
      '20% / 50%',
      '20px/1',
    ];
    for (let i = 0; i < valids.length; i++) {
      const value = valids[i];
      assertCreateEquals(
          `background:${value};`,  // expected
          {'background': value});
    }

    const invalids = [
      '',
      'expression(alert(1))',
      '"',
      '"\'"\'',
      Const.from('red;'),
    ];
    for (let i = 0; i < invalids.length; i++) {
      const value = invalids[i];
      assertThrows(() => {
        SafeStyle.create({'background': value});
      });
    }
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testCreate_withMonkeypatchedObjectPrototype() {
    stubs.set(Object.prototype, 'foo', 'bar');
    assertCreateEquals(
        'background:url(i.png);margin:0;',
        {'background': Const.from('url(i.png)'), 'margin': '0'});
  },

  testConcat() {
    const width = SafeStyle.fromConstant(Const.from('width: 1em;'));
    const margin = SafeStyle.create({'margin': '0'});
    const padding = SafeStyle.create({'padding': '0'});

    let style = SafeStyle.concat(width, margin);
    assertEquals('width: 1em;margin:0;', SafeStyle.unwrap(style));

    style = SafeStyle.concat([width, margin]);
    assertEquals('width: 1em;margin:0;', SafeStyle.unwrap(style));

    style = SafeStyle.concat([width], [padding, margin]);
    assertEquals('width: 1em;padding:0;margin:0;', SafeStyle.unwrap(style));
  },

  testConcat_allowsEmpty() {
    const empty = SafeStyle.EMPTY;
    assertEquals(empty, SafeStyle.concat());
    assertEquals(empty, SafeStyle.concat([]));
    assertEquals(empty, SafeStyle.concat(empty));
  },
});
