// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: lines_longer_than_80_chars

import 'package:csslib/src/messages.dart';
import 'package:csslib/visitor.dart';
import 'package:test/test.dart';

import 'testing.dart';

void expectCss(String css, String expected) {
  var errors = <Message>[];
  var styleSheet = parseCss(css, errors: errors, opts: simpleOptions);
  expect(styleSheet, isNotNull);
  expect(errors, isEmpty);
  expect(prettyPrint(styleSheet), expected);
}

void testSimpleTerms() {
  var errors = <Message>[];
  final input = r'''
@ import url("test.css");
.foo {
  background-color: #191919;
  content: "u+0041";
  width: 10PX;
  height: 22mM !important;
  border-width: 20cm;
  margin-width: 33%;
  border-height: 30EM;
  width: .6in;
  length: 1.2in;
  -web-stuff: -10Px;
}''';
  final generated = r'''
@import "test.css";
.foo {
  background-color: #191919;
  content: "u+0041";
  width: 10px;
  height: 22mm !important;
  border-width: 20cm;
  margin-width: 33%;
  border-height: 30em;
  width: .6in;
  length: 1.2in;
  -web-stuff: -10px;
}''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  final input2 = r'''
* {
  border-color: green;
}''';
  final generated2 = r'''
* {
  border-color: #008000;
}''';

  stylesheet = parseCss(input2, errors: errors..clear());

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated2);

  // Regression test to ensure invalid percentages don't throw an exception and
  // instead print a useful error message when not in checked mode.
  var css = '''
.foo {
  width: Infinity%;
}''';
  stylesheet = parseCss(css, errors: errors..clear(), opts: simpleOptions);
  expect(errors, isNotEmpty);
  expect(errors.first.message, 'expected }, but found %');
  expect(errors.first.span!.text, '%');
}

/// Declarations with comments, references with single-quotes, double-quotes,
/// no quotes.  Hex values with # and letters, and functions (rgba, url, etc.)
void testDeclarations() {
  var errors = <Message>[];
  final input = r'''
.more {
  color: white;
  color: black;
  color: cyan;
  color: red;
  color: #aabbcc;  /* test -- 3 */
  color: blue;
  background-image: url(http://test.jpeg);
  background-image: url("http://double_quote.html");
  background-image: url('http://single_quote.html');
  color: rgba(10,20,255);  <!-- test CDO/CDC  -->
  color: #123aef;   /* hex # part integer and part identifier */
}''';
  final generated = r'''
.more {
  color: #fff;
  color: #000;
  color: #0ff;
  color: #f00;
  color: #abc;
  color: #00f;
  background-image: url("http://test.jpeg");
  background-image: url("http://double_quote.html");
  background-image: url("http://single_quote.html");
  color: rgba(10, 20, 255);
  color: #123aef;
}''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testIdentifiers() {
  var errors = <Message>[];
  final input = r'''
#da {
  height: 100px;
}
#foo {
  width: 10px;
  color: #ff00cc;
}
''';
  final generated = r'''
#da {
  height: 100px;
}
#foo {
  width: 10px;
  color: #f0c;
}''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testComposites() {
  var errors = <Message>[];
  final input = r'''
.xyzzy {
  border: 10px 80px 90px 100px;
  width: 99%;
}
@-webkit-keyframes pulsate {
  0% {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
}''';
  final generated = r'''
.xyzzy {
  border: 10px 80px 90px 100px;
  width: 99%;
}
@-webkit-keyframes pulsate {
  0% {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
}''';

  var stylesheet = parseCss(input, errors: errors);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testUnits() {
  var errors = <Message>[];
  final input = r'''
#id-1 {
  transition: color 0.4s;
  animation-duration: 500ms;
  top: 1em;
  left: 200ex;
  right: 300px;
  bottom: 400cm;
  border-width: 2.5mm;
  margin-top: -.5in;
  margin-left: +5pc;
  margin-right: 5ex;
  margin-bottom: 5ch;
  font-size: 10pt;
  padding-top: 22rem;
  padding-left: 33vw;
  padding-right: 34vh;
  padding-bottom: 3vmin;
  transform: rotate(20deg);
  voice-pitch: 10hz;
  height: 4lh;
  width: 40rlh;
}
#id-2 {
  left: 2fr;
  font-size: 10vmax;
  transform: rotatex(20rad);
  voice-pitch: 10khz;
  -web-kit-resolution: 2dpi;    /* Bogus property name testing dpi unit. */
}
#id-3 {
  -web-kit-resolution: 3dpcm;   /* Bogus property name testing dpi unit. */
  transform: rotatey(20grad);
}
#id-4 {
  -web-kit-resolution: 4dppx;   /* Bogus property name testing dpi unit. */
  transform: rotatez(20turn);
}
''';

  final generated = r'''
#id-1 {
  transition: color 0.4s;
  animation-duration: 500ms;
  top: 1em;
  left: 200ex;
  right: 300px;
  bottom: 400cm;
  border-width: 2.5mm;
  margin-top: -.5in;
  margin-left: +5pc;
  margin-right: 5ex;
  margin-bottom: 5ch;
  font-size: 10pt;
  padding-top: 22rem;
  padding-left: 33vw;
  padding-right: 34vh;
  padding-bottom: 3vmin;
  transform: rotate(20deg);
  voice-pitch: 10hz;
  height: 4lh;
  width: 40rlh;
}
#id-2 {
  left: 2fr;
  font-size: 10vmax;
  transform: rotatex(20rad);
  voice-pitch: 10khz;
  -web-kit-resolution: 2dpi;
}
#id-3 {
  -web-kit-resolution: 3dpcm;
  transform: rotatey(20grad);
}
#id-4 {
  -web-kit-resolution: 4dppx;
  transform: rotatez(20turn);
}''';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testNoValues() {
  var errors = <Message>[];
  final input = r'''
.foo {
  color: ;
}
.bar {
  font:;
  color: blue;
}
''';

  final generated = r'''
.foo {
  color: ;
}
.bar {
  font: ;
  color: #00f;
}''';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testUnicode() {
  var errors = <Message>[];
  final input = r'''
.toggle:after {
  content: '✔';
  line-height: 43px;
  font-size: 20px;
  color: #d9d9d9;
  text-shadow: 0 -1px 0 #bfbfbf;
}
''';

  final generated = r'''
.toggle:after {
  content: '✔';
  line-height: 43px;
  font-size: 20px;
  color: #d9d9d9;
  text-shadow: 0 -1px 0 #bfbfbf;
}''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testNewerCss() {
  var errors = <Message>[];
  final input = r'''
@media screen,print {
  .foobar_screen {
    width: 10px;
  }
}
@page {
  height: 22px;
  size: 3in 3in;
}
@page : left {
  width: 10px;
}
@page bar : left { @top-left { margin: 8px; } }
@page { @top-left { margin: 8px; } width: 10px; }
@charset "ISO-8859-1";
@charset 'ASCII';''';

  final generated = r'''
@media screen, print {
  .foobar_screen {
    width: 10px;
  }
}
@page {
  height: 22px;
  size: 3in 3in;
}
@page:left {
  width: 10px;
}
@page bar:left {
  @top-left {
    margin: 8px;
  }
}
@page {
  @top-left {
    margin: 8px;
  }
  width: 10px;
}
@charset "ISO-8859-1";
@charset "ASCII";''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testMediaQueries() {
  var errors = <Message>[];
  var input = '''
@media screen and (-webkit-min-device-pixel-ratio:0) {
  .todo-item .toggle {
    background: none;
  }
  #todo-item .toggle {
    height: 40px;
  }
}''';
  var generated = '''
@media screen AND (-webkit-min-device-pixel-ratio:0) {
  .todo-item .toggle {
    background: none;
  }
  #todo-item .toggle {
    height: 40px;
  }
}''';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  input = '''
  @media handheld and (min-width: 20em),
         screen and (min-width: 20em) {
    #id { color: red; }
    .myclass { height: 20px; }
  }
  @media print and (min-resolution: 300dpi) {
    #anotherId {
      color: #fff;
    }
  }
  @media print and (min-resolution: 280dpcm) {
    #finalId {
      color: #aaa;
    }
    .class2 {
      border: 20px;
    }
  }''';
  generated = '''
@media handheld AND (min-width:20em), screen AND (min-width:20em) {
  #id {
    color: #f00;
  }
  .myclass {
    height: 20px;
  }
}
@media print AND (min-resolution:300dpi) {
  #anotherId {
    color: #fff;
  }
}
@media print AND (min-resolution:280dpcm) {
  #finalId {
    color: #aaa;
  }
  .class2 {
    border: 20px;
  }
}''';

  stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  input = '''
@media only screen and (min-device-width: 4000px) and
    (min-device-height: 2000px), screen AND (another: 100px) {
      html {
        font-size: 10em;
      }
    }''';
  generated = '''
@media ONLY screen AND (min-device-width:4000px) AND (min-device-height:2000px), screen AND (another:100px) {
  html {
    font-size: 10em;
  }
}''';

  stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  input = '''
@media screen,print AND (min-device-width: 4000px) and
    (min-device-height: 2000px), screen AND (another: 100px) {
      html {
        font-size: 10em;
      }
    }''';
  generated = '@media screen, print AND (min-device-width:4000px) AND '
      '(min-device-height:2000px), screen AND (another:100px) {\n'
      '  html {\n    font-size: 10em;\n  }\n}';

  stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  input = '''
@import "test.css" ONLY screen, NOT print AND (min-device-width: 4000px);''';
  generated = '@import "test.css" ONLY screen, '
      'NOT print AND (min-device-width:4000px);';

  stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  var css = '@media (min-device-width:400px) {\n}';
  expectCss(css, css);

  css = '@media all AND (tranform-3d), (-webkit-transform-3d) {\n}';
  expectCss(css, css);

  // Test that AND operator is required between media type and expressions.
  css = '@media screen (min-device-width:400px';
  stylesheet = parseCss(css, errors: errors..clear(), opts: simpleOptions);
  expect(errors, isNotEmpty);
  expect(
      errors.first.message, contains('expected { after media before ruleset'));
  expect(errors.first.span!.text, '(');

  // Test nested at-rules.
  input = '''
@media (min-width: 840px) {
  .cell {
    width: calc(33% - 16px);
  }
  @supports (display: grid) {
    .cell {
      grid-column-end: span 4;
    }
  }
}''';
  generated = '''
@media (min-width:840px) {
  .cell {
    width: calc(33% - 16px);
  }
  @supports (display: grid) {
    .cell {
      grid-column-end: span 4;
    }
  }
}''';
  expectCss(input, generated);
}

void testMozDocument() {
  var errors = <Message>[];
  // Test empty url-prefix, commonly used for browser detection.
  var css = '''
@-moz-document url-prefix() {
  div {
    color: #000;
  }
}''';
  var expected = '''
@-moz-document url-prefix() {
  div {
    color: #000;
  }
}''';
  var styleSheet = parseCss(css, errors: errors);
  expect(styleSheet, isNotNull);
  expect(errors, isEmpty);
  expect(prettyPrint(styleSheet), expected);

  // Test url-prefix with unquoted parameter
  css = '''
@-moz-document url-prefix(http://www.w3.org/Style/) {
  div {
    color: #000;
  }
}''';
  expected = '''
@-moz-document url-prefix("http://www.w3.org/Style/") {
  div {
    color: #000;
  }
}''';
  styleSheet = parseCss(css, errors: errors);
  expect(styleSheet, isNotNull);
  expect(errors, isEmpty);
  expect(prettyPrint(styleSheet), expected);

  // Test domain with unquoted parameter
  css = '''
@-moz-document domain(google.com) {
  div {
    color: #000;
  }
}''';
  expected = '''
@-moz-document domain("google.com") {
  div {
    color: #000;
  }
}''';
  styleSheet = parseCss(css, errors: errors);
  expect(styleSheet, isNotNull);
  expect(errors, isEmpty);
  expect(prettyPrint(styleSheet), expected);

  // Test all document functions combined.
  css = '@-moz-document '
      'url(http://www.w3.org/), '
      "url-prefix('http://www.w3.org/Style/'), "
      'domain("google.com"), '
      'regexp("https:.*") { div { color: #000; } }';
  expected = '@-moz-document '
      'url("http://www.w3.org/"), '
      'url-prefix("http://www.w3.org/Style/"), '
      'domain("google.com"), '
      'regexp("https:.*") {\n  div {\n    color: #000;\n  }\n}';
  styleSheet = parseCss(css, errors: errors);
  expect(styleSheet, isNotNull);
  expect(errors, isEmpty);
  expect(prettyPrint(styleSheet), expected);
}

void testSupports() {
  // Test single declaration condition.
  var css = '''
@supports (-webkit-appearance: none) {
  div {
    -webkit-appearance: none;
  }
}''';
  var expected = '''
@supports (-webkit-appearance: none) {
  div {
    -webkit-appearance: none;
  }
}''';
  expectCss(css, expected);

  // Test negation.
  css = '''
@supports not ( display: flex ) {
  body { width: 100%; }
}''';
  expected = '''
@supports not (display: flex) {
  body {
    width: 100%;
  }
}''';
  expectCss(css, expected);

  // Test clause with multiple conditions.
  css = '''
@supports (box-shadow: 0 0 2px black inset) or
    (-moz-box-shadow: 0 0 2px black inset) or
    (-webkit-box-shadow: 0 0 2px black inset) or
    (-o-box-shadow: 0 0 2px black inset) {
  .box {
    box-shadow: 0 0 2px black inset;
  }
}''';
  expected = '@supports (box-shadow: 0 0 2px #000 inset) or '
      '(-moz-box-shadow: 0 0 2px #000 inset) or '
      '(-webkit-box-shadow: 0 0 2px #000 inset) or '
      '(-o-box-shadow: 0 0 2px #000 inset) {\n'
      '  .box {\n'
      '    box-shadow: 0 0 2px #000 inset;\n'
      '  }\n'
      '}';
  expectCss(css, expected);

  // Test conjunction and disjunction together.
  css = '''
@supports ((transition-property: color) or (animation-name: foo)) and
    (transform: rotate(10deg)) {
  div {
    transition-property: color;
    transform: rotate(10deg);
  }
}''';

  expected = '@supports '
      '((transition-property: color) or (animation-name: foo)) and '
      '(transform: rotate(10deg)) {\n'
      '  div {\n'
      '    transition-property: color;\n'
      '    transform: rotate(10deg);\n'
      '  }\n'
      '}';
  expectCss(css, expected);

  // Test that operators can't be mixed without parentheses.
  css = '@supports (a: 1) and (b: 2) or (c: 3) {}';
  var errors = <Message>[];
  var styleSheet = parseCss(css, errors: errors, opts: simpleOptions);
  expect(styleSheet, isNotNull);
  expect(errors, isNotEmpty);
  expect(errors.first.message,
      "Operators can't be mixed without a layer of parentheses");
  expect(errors.first.span!.text, 'or');
}

void testViewport() {
  // No declarations.
  var css = '@viewport {\n}';
  expectCss(css, css);

  // All declarations.
  css = '''
@viewport {
  min-width: auto;
  max-width: 800px;
  width: 400px;
  min-height: 50%;
  max-height: 200px;
  height: 100px 200px;
  zoom: auto;
  min-zoom: 0.75;
  max-zoom: 40%;
  user-zoom: fixed;
  orientation: landscape;
}''';
  expectCss(css, css);

  // Vendor specific.
  css = '''
@-ms-viewport {
  width: device-width;
}''';
  expectCss(css, css);
}

void testFontFace() {
  var errors = <Message>[];

  final input = '''
@font-face {
  font-family: BBCBengali;
  src: url(fonts/BBCBengali.ttf) format("opentype");
  unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
}''';
  final generated = '''@font-face {
  font-family: BBCBengali;
  src: url("fonts/BBCBengali.ttf") format("opentype");
  unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
}''';
  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  final input1 = '''
@font-face {
  font-family: Gentium;
  src: url(http://example.com/fonts/Gentium.ttf);
  src: url(http://example.com/fonts/Gentium.ttf);
}''';
  final generated1 = '''@font-face {
  font-family: Gentium;
  src: url("http://example.com/fonts/Gentium.ttf");
  src: url("http://example.com/fonts/Gentium.ttf");
}''';

  stylesheet = parseCss(input1, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated1);

  final input2 = '''
@font-face {
src: url(ideal-sans-serif.woff) format("woff"),
     url(basic-sans-serif.ttf) format("opentype"),
     local(Gentium Bold);
}''';
  final generated2 = '@font-face {\n'
      '  src: url("ideal-sans-serif.woff") '
      'format("woff"), url("basic-sans-serif.ttf") '
      'format("opentype"), local(Gentium Bold);\n}';

  stylesheet = parseCss(input2, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated2);

  final input3 = '''
@font-face {
  font-family: MyGentium Text Ornaments;
  src: local(Gentium Bold),   /* full font name */
       local(Gentium-Bold),   /* Postscript name */
       url(GentiumBold.ttf);  /* otherwise, download it */
  font-weight: bold;
}''';
  final generated3 = '''
@font-face {
  font-family: MyGentium Text Ornaments;
  src: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
  font-weight: bold;
}''';

  stylesheet = parseCss(input3, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated3);

  final input4 = '''
@font-face {
  font-family: STIXGeneral;
  src: local(STIXGeneral), url(/stixfonts/STIXGeneral.otf);
  unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
}''';
  final generated4 = '''@font-face {
  font-family: STIXGeneral;
  src: local(STIXGeneral), url("/stixfonts/STIXGeneral.otf");
  unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
}''';
  stylesheet = parseCss(input4, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated4);
}

void testFontFamily() {
  test('quoted', () {
    var errors = <Message>[];
    var stylesheet = parseCss('''
body {
  font-family: "Arial Narrow";
}''', errors: errors..clear(), opts: simpleOptions);
    expect(errors.isEmpty, true, reason: errors.toString());
    expect(prettyPrint(stylesheet), '''
body {
  font-family: "Arial Narrow";
}''');
    var ruleSet = stylesheet.topLevels.first as RuleSet;
    var declaration =
        ruleSet.declarationGroup.declarations.first as Declaration;
    var expressions = declaration.expression as Expressions;
    expect(declaration.property, 'font-family');
    expect(printExpressions(expressions), '"Arial Narrow"');
  });

  test('without quotes', () {
    var errors = <Message>[];
    var stylesheet = parseCss('''
body {
  font-family: Arial Narrow;
}''', errors: errors..clear(), opts: simpleOptions);
    expect(errors.isEmpty, true, reason: errors.toString());
    expect(prettyPrint(stylesheet), '''
body {
  font-family: Arial Narrow;
}''');
    var ruleSet = stylesheet.topLevels.first as RuleSet;
    var declaration =
        ruleSet.declarationGroup.declarations.first as Declaration;
    var expressions = declaration.expression as Expressions;
    expect(declaration.property, 'font-family');
    expect(printExpressions(expressions), 'Arial Narrow');
  });

  test('starts with identifier', () {
    var errors = <Message>[];
    var stylesheet = parseCss('''
body {
  font-family: PT Sans;
}''', errors: errors..clear(), opts: simpleOptions);
    expect(errors.isEmpty, true, reason: errors.toString());
    expect(prettyPrint(stylesheet), '''
body {
  font-family: PT Sans;
}''');
    var ruleSet = stylesheet.topLevels.first as RuleSet;
    var declaration =
        ruleSet.declarationGroup.declarations.first as Declaration;
    var expressions = declaration.expression as Expressions;
    expect(declaration.property, 'font-family');
    expect(printExpressions(expressions), 'PT Sans');
  });
}

void testCssFile() {
  var errors = <Message>[];
  final input = r'''
@import 'simple.css'
@import "test.css" print
@import url(test.css) screen, print
@import url(http://google.com/maps/maps.css);

div[href^='test'] {
  height: 10px;
}

@-webkit-keyframes pulsate {
  from {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
  10% {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
  30% {
    -webkit-transform: translate3d(0, 2, 0) scale(1.0);
  }
}

.foobar {
    grid-columns: 10px ("content" 1fr 10px)[4];
}

.test-background {
  background:  url(http://www.foo.com/bar.png);
}

.test-background-with-multiple-properties {
  background: #000 url(http://www.foo.com/bar.png);
}
''';

  final generated = '''
@import "simple.css";
@import "test.css" print;
@import "test.css" screen, print;
@import "http://google.com/maps/maps.css";
div[href^="test"] {
  height: 10px;
}
@-webkit-keyframes pulsate {
  from {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
  10% {
    -webkit-transform: translate3d(0, 0, 0) scale(1.0);
  }
  30% {
    -webkit-transform: translate3d(0, 2, 0) scale(1.0);
  }
}
.foobar {
  grid-columns: 10px ("content" 1fr 10px) [4];
}
.test-background {
  background: url("http://www.foo.com/bar.png");
}
.test-background-with-multiple-properties {
  background: #000 url("http://www.foo.com/bar.png");
}''';
  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testCompactEmitter() {
  var errors = <Message>[];

  // Check !import compactly emitted.
  final input = r'''
div {
  color: green !important;
  background: red blue green;
}
.foo p[bar] {
  color: blue;
}
@page {
  @top-left {
    color: red;
  }
}
@page : first{}
@page foo : first {}
@media screen AND (max-width: 800px) {
  div {
    font-size: 24px;
  }
}
@keyframes foo {
  0% {
    transform: scaleX(0);
  }
}
div {
  color: rgba(0, 0, 0, 0.2);
}
''';
  final generated = '''
div{color:green!important;background:red blue green}.foo p[bar]{color:blue}@page{@top-left{color:red}}@page:first{}@page foo:first{}@media screen AND (max-width:800px){div{font-size:24px}}@keyframes foo{0%{transform:scaleX(0)}}div{color:rgba(0,0,0,0.2)}''';

  var stylesheet = parseCss(input, errors: errors);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(compactOutput(stylesheet), generated);

  // Check namespace directive compactly emitted.
  final input2 = '@namespace a url(http://www.example.org/a);';
  final generated2 = '@namespace a url(http://www.example.org/a);';

  var stylesheet2 = parseCss(input2, errors: errors..clear());

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(compactOutput(stylesheet2), generated2);
}

void testNotSelectors() {
  var errors = <Message>[];

  final input = r'''
.details:not(.open-details) x-element,
.details:not(.open-details) .summary {
  overflow: hidden;
}

.details:not(.open-details) x-icon {
  margin-left: 99px;
}

.kind-class .details:not(.open-details) x-icon {
  margin-left: 0px;
}

.name {
  margin-left: 0px;
}

.details:not(.open-details) .the-class {
  width: 80px;
}

*:focus
{
  outline: none;
}

body > h2:not(:first-of-type):not(:last-of-type) {
  color: red;
}

.details-1:not([DISABLED]) {
  outline: none;
}

html|*:not(:link):not(:visited) {
  width: 92%;
}

*|*:not(*) {
  font-weight: bold;
}

*:not(:not([disabled])) { color: blue; }
''';
  final generated = r'''
.details:not(.open-details) x-element, .details:not(.open-details) .summary {
  overflow: hidden;
}
.details:not(.open-details) x-icon {
  margin-left: 99px;
}
.kind-class .details:not(.open-details) x-icon {
  margin-left: 0px;
}
.name {
  margin-left: 0px;
}
.details:not(.open-details) .the-class {
  width: 80px;
}
*:focus {
  outline: none;
}
body > h2:not(:first-of-type):not(:last-of-type) {
  color: #f00;
}
.details-1:not([DISABLED]) {
  outline: none;
}
html|*:not(:link):not(:visited) {
  width: 92%;
}
*|*:not(*) {
  font-weight: bold;
}
*:not(:not([disabled])) {
  color: #00f;
}''';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testIE() {
  var errors = <Message>[];
  final input = '.test {\n'
      '  filter: progid:DXImageTransform.Microsoft.gradient'
      "(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');\n"
      '}';
  final generated = '.test {\n'
      '  filter: progid:DXImageTransform.Microsoft.gradient'
      "(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');\n"
      '}';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);

  final input2 = '.test {\n'
      '  filter: progid:DXImageTransform.Microsoft.gradient'
      "(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670')\n"
      '        progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);\n'
      '}';

  final generated2 = '.test {\n'
      '  filter: progid:DXImageTransform.Microsoft.gradient'
      "(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670')\n"
      '         progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);\n'
      '}';

  stylesheet = parseCss(input2, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated2);

  final input3 = '''
div {
  filter: alpha(opacity=80);          /* IE7 and under */
  -ms-filter: "Alpha(Opacity=40)";    /* IE8 and newer */

  Filter: Blur(Add = 0, Direction = 225, Strength = 10);
  Filter: FlipV;
  Filter: Gray;
  FILTER: Chroma(Color = #000000) Mask(Color=#00FF00);
  Filter: Alpha(Opacity=100, FinishOpacity=0, Style=2, StartX=20, StartY=40,
      FinishX=0, FinishY=0) Wave(Add=0, Freq=5, LightStrength=20,
      Phase=220, Strength=10);
}
''';
  final generated3 = 'div {\n  filter: alpha(opacity=80);\n'
      '  -ms-filter: "Alpha(Opacity=40)";\n'
      '  Filter: Blur(Add = 0, Direction = 225, Strength = 10);\n'
      '  Filter: FlipV;\n  Filter: Gray;\n'
      '  FILTER: Chroma(Color = #000000)  Mask(Color=#00FF00);\n'
      '  Filter: Alpha(Opacity=100, FinishOpacity=0, Style=2, '
      'StartX=20, StartY=40,\n'
      '      FinishX=0, FinishY=0)  Wave(Add=0, Freq=5, LightStrength=20,\n'
      '      Phase=220, Strength=10);\n}';

  stylesheet = parseCss(input3, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated3);

  final input4 = '''
div {
  filter: FlipH;
}''';

  stylesheet = parseCss(input4, errors: errors..clear(), opts: simpleOptions);

  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), input4);
}

///  Test IE specific declaration syntax:
///    IE6 property name prefixed with _ (normal CSS property name can start
///    with an underscore).
///
///    IE7 or below property add asterisk before the CSS property.
///
///    IE8 or below add \9 at end of declaration expression e.g.,
///        background: red\9;
void testIEDeclaration() {
  var errors = <Message>[];

  final input = '''
.testIE-6 {
  _zoom : 5;
}
.clearfix {
  *zoom: 1;
}
audio, video {
  display: inline-block;
  *display: inline;
  *zoom: 1;
}
input {
  *overflow: visible;
  line-height: normal;
}
.uneditable-input:focus {
  border-color: rgba(82, 168, 236, 0.8);
  outline: 0;
  outline: thin dotted \\9; /* IE6-9 */
}

input[type="radio"], input[type="checkbox"] {
  margin-top: 1px \\9;
  *margin-top: 0;
}

input.search-query {
  padding-right: 14px;
  padding-right: 4px \\9;
  padding-left: 14px;
  padding-left: 4px \\9; /* IE7-8 no border-radius, don't indent padding. */
}

.btn.active {
  background-color: #cccccc \\9;
}

@-webkit-keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}

@-moz-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}

@-ms-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}

@-o-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}

@keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}''';

  final generated = '''
.testIE-6 {
  _zoom: 5;
}
.clearfix {
  *zoom: 1;
}
audio, video {
  display: inline-block;
  *display: inline;
  *zoom: 1;
}
input {
  *overflow: visible;
  line-height: normal;
}
.uneditable-input:focus {
  border-color: rgba(82, 168, 236, 0.8);
  outline: 0;
  outline: thin dotted \\9;
}
input[type="radio"], input[type="checkbox"] {
  margin-top: 1px \\9;
  *margin-top: 0;
}
input.search-query {
  padding-right: 14px;
  padding-right: 4px \\9;
  padding-left: 14px;
  padding-left: 4px \\9;
}
.btn.active {
  background-color: #ccc \\9;
}
@-webkit-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}
@-moz-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}
@keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}
@-o-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}
@keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
}''';

  var stylesheet = parseCss(input, errors: errors, opts: simpleOptions);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void testHangs() {
  var errors = <Message>[];

  // Bad hexvalue had caused a hang in processTerm.
  final input = r'''#a { color: #ebebeburl(0/IE8+9+); }''';
  parseCss(input, errors: errors, opts: options);

  expect(errors.length, 3, reason: errors.toString());

  var errorMessage = errors[0];
  expect(errorMessage.message, contains('Bad hex number'));
  expect(errorMessage.span, isNotNull);
  expect(errorMessage.span!.start.line, 0);
  expect(errorMessage.span!.start.column, 12);
  expect(errorMessage.span!.text, '#ebebeburl');

  errorMessage = errors[1];
  expect(errorMessage.message, contains('expected }, but found +'));
  expect(errorMessage.span, isNotNull);
  expect(errorMessage.span!.start.line, 0);
  expect(errorMessage.span!.start.column, 30);
  expect(errorMessage.span!.text, '+');

  errorMessage = errors[2];
  expect(errorMessage.message, contains('premature end of file unknown CSS'));
  expect(errorMessage.span, isNotNull);
  expect(errorMessage.span!.start.line, 0);
  expect(errorMessage.span!.start.column, 31);
  expect(errorMessage.span!.text, ')');

  // Missing closing parenthesis for keyframes.
  final input2 = r'''@-ms-keyframes progress-bar-stripes {
  from {
    background-position: 40px 0;
  }
  to {
    background-position: 0 0;
  }
''';

  parseCss(input2, errors: errors..clear(), opts: options);

  expect(errors.length, 1, reason: errors.toString());

  errorMessage = errors[0];
  expect(errorMessage.message, contains('unexpected end of file'));
  expect(errorMessage.span, isNotNull);
  expect(errorMessage.span!.start.line, 7);
  expect(errorMessage.span!.start.column, 0);
  expect(errorMessage.span!.text.trim(), '');
}

void testExpressionSpans() {
  final input = r'''.foo { width: 50px; }''';

  var stylesheet = parseCss(input);
  var ruleSet = stylesheet.topLevels.single as RuleSet;

  var declaration = ruleSet.declarationGroup.declarations.single as Declaration;
  expect(declaration.span.text, 'width: 50px');

  var expressions = declaration.expression as Expressions;
  expect(expressions.expressions.first.span!.text, '50px');
}

void testComments() {
  final css = '''/* This comment has a nested HTML comment...
* <html>
*   <!-- Nested HTML comment... -->
*   <div></div>
* </html>
*/''';
  expectCss(css, '');
}

void simpleCalc() {
  final input = r'''.foo { height: calc(100% - 55px); }''';
  var stylesheet = parseCss(input);
  var decl = (stylesheet.topLevels.single as RuleSet)
      .declarationGroup
      .declarations
      .single;
  expect(decl.span!.text, 'height: calc(100% - 55px)');
}

void complexCalc() {
  final input = r'''.foo { left: calc((100%/3 - 2) * 1em - 2 * 1px); }''';
  var stylesheet = parseCss(input);
  var decl = (stylesheet.topLevels.single as RuleSet)
      .declarationGroup
      .declarations
      .single;
  expect(decl.span!.text, 'left: calc((100%/3 - 2) * 1em - 2 * 1px)');
}

void twoCalcs() {
  final input = r'''.foo { margin: calc(1rem - 2px) calc(1rem - 1px); }''';
  var stylesheet = parseCss(input);
  var decl = (stylesheet.topLevels.single as RuleSet)
      .declarationGroup
      .declarations
      .single;
  expect(decl.span!.text, 'margin: calc(1rem - 2px) calc(1rem - 1px)');
}

void selectorWithCalcs() {
  var errors = <Message>[];
  final input = r'''
.foo {
  width: calc(1em + 5 * 2em);
  height: calc(1px + 2%) !important;
  border: 5px calc(1pt + 2cm) 6px calc(1em + 1in + 2px) red;
  border: calc(5px + 1em) 0px 1px calc(10 + 20 + 1px);
  margin: 25px calc(50px + (100% / (3 - 1em) - 20%)) calc(10px + 10 * 20) calc(100% - 10px);
}''';
  final generated = r'''
.foo {
  width: calc(1em + 5 * 2em);
  height: calc(1px + 2%) !important;
  border: 5px calc(1pt + 2cm) 6px calc(1em + 1in + 2px) #f00;
  border: calc(5px + 1em) 0px 1px calc(10 + 20 + 1px);
  margin: 25px calc(50px + (100% / (3 - 1em) - 20%)) calc(10px + 10 * 20) calc(100% - 10px);
}''';

  var stylesheet = parseCss(input, errors: errors);
  expect(errors.isEmpty, true, reason: errors.toString());
  expect(prettyPrint(stylesheet), generated);
}

void vendorPrefixedCalc() {
  var css = '''
.foo {
  width: -webkit-calc((100% - 15px*1) / 1);
}''';
  expectCss(css, css);

  css = '''
.foo {
  width: -moz-calc((100% - 15px*1) / 1);
}''';
  expectCss(css, css);
}

void main() {
  test('Simple Terms', testSimpleTerms);
  test('Declarations', testDeclarations);
  test('Identifiers', testIdentifiers);
  test('Composites', testComposites);
  test('Units', testUnits);
  test('No Values', testNoValues);
  test('Unicode', testUnicode);
  test('Newer CSS', testNewerCss);
  test('Media Queries', testMediaQueries);
  test('Document', testMozDocument);
  test('Supports', testSupports);
  test('Viewport', testViewport);
  test('Font-Face', testFontFace);
  group('font-family', testFontFamily);
  test('CSS file', testCssFile);
  test('Compact Emitter', testCompactEmitter);
  test('Selector Negation', testNotSelectors);
  test('IE stuff', testIE);
  test('IE declaration syntax', testIEDeclaration);
  test('Hanging bugs', testHangs);
  test('Expression spans', testExpressionSpans);
  test('Comments', testComments);
  group('calc function', () {
    test('simple calc', simpleCalc);
    test('single complex', complexCalc);
    test('two calc terms for same declaration', twoCalcs);
    test('selector with many calc declarations', selectorWithCalcs);
    test('vendor prefixed calc', vendorPrefixedCalc);
  });
}
