content: // Copyright 2018 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  @TestOn('vm')
  library;
  
  import 'package:charcode/charcode.dart';
  import 'package:source_maps/source_maps.dart';
  import 'package:source_span/source_span.dart';
  import 'package:string_scanner/string_scanner.dart';
  import 'package:test/test.dart';
  
  import 'package:sass/sass.dart';
  import 'package:sass/src/utils.dart';
  
  import 'dart_api/test_importer.dart';
  
  void main() {
    group("maps source to target for", () {
      group("a style rule", () {
        test("that's basic", () {
          _expectSourceMap(
            """
              {{1}}foo
                {{2}}bar: baz
            """,
            """
              {{1}}foo {
                {{2}}bar: baz;
              }
            """,
            """
              {{1}}foo {
                {{2}}bar: baz;
              }
            """,
          );
        });
  
        test("with a multiline selector", () {
          _expectSourceMap(
            """
              {{1}}foo,
              bar
                {{2}}bar: baz
            """,
            """
              {{1}}foo,
              bar {
                {{2}}bar: baz;
              }
            """,
            """
              {{1}}foo,
              {{1}}bar {
                {{2}}bar: baz;
              }
            """,
          );
        });
  
        test("with a property on a different line", () {
          _expectScssSourceMap(
            """
              {{1}}foo {
                {{2}}bar:
                    {{3}}baz;
              }
            """,
            """
              {{1}}foo {
                {{2}}bar: {{3}}baz;
              }
            """,
          );
        });
  
        test("with a multiline property", () {
          _expectScssSourceMap(
            """
              {{1}}foo {
                {{2}}bar: baz
                    bang;
              }
            """,
            """
              {{1}}foo {
                {{2}}bar: baz bang;
              }
            """,
          );
        });
  
        test("that's nested", () {
          _expectSourceMap(
            """
              foo
                {{1}}bar
                  {{2}}baz: bang
            """,
            """
              foo {
                {{1}}bar {
                  {{2}}baz: bang;
                }
              }
            """,
            """
              {{1}}foo bar {
                {{2}}baz: bang;
              }
            """,
          );
        });
  
        test("with a nested rule and declaration", () {
          _expectSourceMap(
            """
              {{1}}foo
                {{2}}a: b
  
                {{3}}bar
                  {{4}}x: y
            """,
            """
              {{1}}foo {
                {{2}}a: b;
  
                {{3}}bar {
                  {{4}}x: y;
                }
              }
            """,
            """
              {{1}}foo {
                {{2}}a: b;
              }
              {{3}}foo bar {
                {{4}}x: y;
              }
            """,
          );
        });
  
        test("with a nested declaration", () {
          _expectSourceMap(
            """
              {{1}}foo
                {{2}}a: b
                  {{3}}c: d
            """,
            """
              {{1}}foo {
                {{2}}a: b {
                  {{3}}c: d;
                }
              }
            """,
            """
              {{1}}foo {
                {{2}}a: b;
                {{3}}a-c: d;
              }
            """,
          );
        });
      });
  
      group("an unknown at-rule", () {
        test("without children", () {
          _expectSourceMap(
            "{{1}}@foo (fblthp)",
            "{{1}}@foo (fblthp);",
            "{{1}}@foo (fblthp);",
          );
        });
  
        group("that contains", () {
          test("declarations", () {
            _expectSourceMap(
              """
                {{1}}@foo (fblthp)
                  {{2}}bar: baz
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}bar: baz;
                }
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}bar: baz;
                }
              """,
            );
          });
  
          test("style rules", () {
            _expectSourceMap(
              """
                {{1}}@foo (fblthp)
                  {{2}}bar
                    {{3}}baz: bang
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}bar {
                    {{3}}baz: bang;
                  }
                }
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}bar {
                    {{3}}baz: bang;
                  }
                }
              """,
            );
          });
  
          test("at-rules", () {
            _expectSourceMap(
              """
                {{1}}@foo (fblthp)
                  {{2}}@bar baz
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}@bar baz;
                }
              """,
              """
                {{1}}@foo (fblthp) {
                  {{2}}@bar baz;
                }
              """,
            );
          });
        });
      });
  
      group("a comment", () {
        test("that covers a single line", () {
          _expectSourceMap(
            """
              {{1}}/* foo bar
              {{2}}/* baz bang
            """,
            """
              {{1}}/* foo bar */
              {{2}}/* baz bang */
            """,
            """
              {{1}}/* foo bar */
              {{2}}/* baz bang */
            """,
          );
        });
  
        test("that covers multiple lines", () {
          _expectSourceMap(
            """
              {{1}}/* foo bar
                 baz bang
            """,
            """
              {{1}}/* foo bar
               * baz bang */
            """,
            """
              {{1}}/* foo bar
              {{1}} * baz bang */
            """,
          );
        });
      });
  
      group("@import", () {
        test("with a single URL", () {
          _expectSourceMap(
            """
              @import {{1}}url(foo)
            """,
            """
              @import {{1}}url(foo);
            """,
            """
              {{1}}@import url(foo);
            """,
          );
        });
  
        test("with multiple URLs", () {
          _expectSourceMap(
            """
              @import {{1}}url(foo), {{2}}"bar.css"
            """,
            """
              @import {{1}}url(foo),
                {{2}}"bar.css";
            """,
            """
              {{1}}@import url(foo);
              {{2}}@import "bar.css";
            """,
          );
        });
      });
  
      test("@keyframes", () {
        _expectSourceMap(
          """
            {{1}}@keyframes name
              {{2}}from
                {{3}}top: 0px
  
              {{4}}10%
                {{5}}top: 10px
          """,
          """
            {{1}}@keyframes name {
              {{2}}from {
                {{3}}top: 0px;
              }
  
              {{4}}10% {
                {{5}}top: 10px;
              }
            }
          """,
          """
            {{1}}@keyframes name {
              {{2}}from {
                {{3}}top: 0px;
              }
              {{4}}10% {
                {{5}}top: 10px;
              }
            }
          """,
        );
      });
  
      group("@media", () {
        test("at the root", () {
          _expectSourceMap(
            """
              {{1}}@media screen
                {{2}}foo
                  {{3}}bar: baz
            """,
            """
              {{1}}@media screen {
                {{2}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
            """
              {{1}}@media screen {
                {{2}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
          );
        });
  
        test("within a style rule", () {
          _expectSourceMap(
            """
              {{1}}foo
                {{2}}@media screen
                  {{3}}bar: baz
            """,
            """
              {{1}}foo {
                {{2}}@media screen {
                  {{3}}bar: baz;
                }
              }
            """,
            """
              {{2}}@media screen {
                {{1}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
          );
        });
      });
  
      group("@supports", () {
        test("at the root", () {
          _expectSourceMap(
            """
              {{1}}@supports (display: grid)
                {{2}}foo
                  {{3}}bar: baz
            """,
            """
              {{1}}@supports (display: grid) {
                {{2}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
            """
              {{1}}@supports (display: grid) {
                {{2}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
          );
        });
  
        test("within a style rule", () {
          _expectSourceMap(
            """
              {{1}}foo
                {{2}}@supports (display: grid)
                  {{3}}bar: baz
            """,
            """
              {{1}}foo {
                {{2}}@supports (display: grid) {
                  {{3}}bar: baz;
                }
              }
            """,
            """
              {{2}}@supports (display: grid) {
                {{1}}foo {
                  {{3}}bar: baz;
                }
              }
            """,
          );
        });
      });
  
      group("a value from a variable defined", () {
        group("in", () {
          test("a variable declaration", () {
            _expectScssSourceMap(
              r"""
                $var: {{1}}value;
  
                {{2}}a {
                  {{3}}b: $var;
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}value;
                }
              """,
            );
          });
  
          test("an @each rule", () {
            _expectScssSourceMap(
              r"""
                @each $var in {{1}}1 2 {
                  {{2}}a {
                    {{3}}b: $var;
                  }
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}1;
                }
  
                {{2}}a {
                  {{3}}b: {{1}}2;
                }
              """,
            );
          });
  
          test("a @for rule", () {
            _expectScssSourceMap(
              r"""
                @for $var from {{1}}1 through 2 {
                  {{2}}a {
                    {{3}}b: $var;
                  }
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}1;
                }
  
                {{2}}a {
                  {{3}}b: {{1}}2;
                }
              """,
            );
          });
  
          group("a mixin argument that is", () {
            test("the default value", () {
              _expectScssSourceMap(
                r"""
                  @mixin foo($var: {{1}}1) {
                    {{2}}b: $var;
                  }
  
                  {{3}}a {
                    @include foo();
                  }
                """,
                """
                  {{3}}a {
                    {{2}}b: {{1}}1;
                  }
                """,
              );
            });
  
            test("passed by position", () {
              _expectScssSourceMap(
                r"""
                  @mixin foo($var) {
                    {{1}}b: $var;
                  }
  
                  {{2}}a {
                    @include foo({{3}}1);
                  }
                """,
                """
                  {{2}}a {
                    {{1}}b: {{3}}1;
                  }
                """,
              );
            });
  
            test("passed by name", () {
              _expectScssSourceMap(
                r"""
                  @mixin foo($var) {
                    {{1}}b: $var;
                  }
  
                  {{2}}a {
                    @include foo($var: {{3}}1);
                  }
                """,
                """
                  {{2}}a {
                    {{1}}b: {{3}}1;
                  }
                """,
              );
            });
  
            test("passed by arglist", () {
              _expectScssSourceMap(
                r"""
                  @mixin foo($var) {
                    {{1}}b: $var;
                  }
  
                  {{2}}a {
                    @include foo({{3}}(1,)...);
                  }
                """,
                """
                  {{2}}a {
                    {{1}}b: {{3}}1;
                  }
                """,
              );
            });
          });
        });
  
        group("in a variable which is referenced by", () {
          test("a variable rename", () {
            _expectScssSourceMap(
              r"""
                $var1: {{1}}value;
                $var2: $var1;
  
                {{2}}a {
                  {{3}}b: $var2;
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}value;
                }
              """,
            );
          });
  
          test("an @each rule from a variable", () {
            _expectScssSourceMap(
              r"""
                $list: {{1}}1 2;
  
                @each $var in $list {
                  {{2}}a {
                    {{3}}b: $var;
                  }
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}1;
                }
  
                {{2}}a {
                  {{3}}b: {{1}}2;
                }
              """,
            );
          });
  
          test("a @for rule from a variable", () {
            _expectScssSourceMap(
              r"""
                $start: {{1}}1;
                $end: 2;
  
                @for $var from $start through $end {
                  {{2}}a {
                    {{3}}b: $var;
                  }
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}1;
                }
  
                {{2}}a {
                  {{3}}b: {{1}}2;
                }
              """,
            );
          });
  
          test("a @use rule with a with clause", () {
            _expectScssSourceMap(
              r"""
                $var1: {{1}}new value;
                @use 'other' with ($var2: $var1);
  
                {{2}}a {
                  {{3}}b: other.$var2;
                }
              """,
              """
                {{2}}a {
                  {{3}}b: {{1}}new value;
                }
              """,
              importer: TestImporter(
                (url) => Uri.parse("u:$url"),
                (_) => ImporterResult(
                  r"$var2: default value !default;",
                  syntax: Syntax.scss,
                ),
              ),
            );
          });
  
          group("a mixin argument that is", () {
            test("the default value", () {
              _expectScssSourceMap(
                r"""
                  $original: {{1}}1;
  
                  @mixin foo($var: $original) {
                    {{2}}b: $var;
                  }
  
                  {{3}}a {
                    @include foo();
                  }
                """,
                """
                  {{3}}a {
                    {{2}}b: {{1}}1;
                  }
                """,
              );
            });
  
            test("passed by position", () {
              _expectScssSourceMap(
                r"""
                  $original: {{1}}1;
  
                  @mixin foo($var) {
                    {{2}}b: $var;
                  }
  
                  {{3}}a {
                    @include foo($original);
                  }
                """,
                """
                  {{3}}a {
                    {{2}}b: {{1}}1;
                  }
                """,
              );
            });
  
            test("passed by name", () {
              _expectScssSourceMap(
                r"""
                  $original: {{1}}1;
  
                  @mixin foo($var) {
                    {{2}}b: $var;
                  }
  
                  {{3}}a {
                    @include foo($var: $original);
                  }
                """,
                """
                  {{3}}a {
                    {{2}}b: {{1}}1;
                  }
                """,
              );
            });
  
            test("passed by arglist", () {
              _expectScssSourceMap(
                r"""
                  $original: {{1}}1;
  
                  @mixin foo($var) {
                    {{2}}b: $var;
                  }
  
                  {{3}}a {
                    @include foo($original...);
                  }
                """,
                """
                  {{3}}a {
                    {{2}}b: {{1}}1;
                  }
                """,
              );
            });
          });
        });
      });
  
      group("a stylesheet with Unicode characters", () {
        test("in expanded mode", () {
          _expectSourceMap(
            """
              {{1}}föö
                {{2}}bär: bäz
            """,
            """
              {{1}}föö {
                {{2}}bär: bäz;
              }
            """,
            """
              @charset "UTF-8";
              {{1}}föö {
                {{2}}bär: bäz;
              }
            """,
          );
        });
  
        test("in compressed mode", () {
          _expectSourceMap(
            """
              {{1}}föö
                {{2}}bär: bäz
            """,
            """
              {{1}}föö {
                {{2}}bär: bäz;
              }
            """,
            "\uFEFF{{1}}föö{{{2}}bär:bäz}",
            style: OutputStyle.compressed,
          );
        });
      });
    });
  
    test("doesn't use the source map location for variable errors", () {
      // When source maps are enabled (by passing a callback to sourceMap), Sass
      // tracks the original location where each variable was declared so that
      // browsers can link to variable declarations rather than just usages.
      // However, we want to refer to the usages when reporting errors because
      // they have more context.
      expect(
        () {
          compileString(r"""
            $map: (a: b);
            x {y: $map}
          """, sourceMap: (_) {});
        },
        throwsA(
          predicate((untypedError) {
            var error = untypedError as SourceSpanException;
            expect(error.span!.text, equals(r"$map"));
            return true;
          }),
        ),
      );
    });
  }
  
  /// Asserts that [sass] and [scss] both compile to [css], and that the
  /// associated source maps are generated properly.
  ///
  /// All three strings are expected to be annotated to indicate which locations
  /// in the source Sass and SCSS should map to which locations in the CSS. This
  /// is done using numbers enclosed in double curly braces called "location
  /// identifiers'. For example, the source text
  ///
  ///     {{1}}foo{{2}}: {{3}}1 + 1{{4}};
  ///
  /// indicates four locations that should be mapped to the locations with the
  /// same numbers in the target text:
  ///
  ///     {{1}}foo{{2}}: {{3}}2{{4}};
  ///
  /// The [css] text may have multiple instances of the same location identifier,
  /// which indicates that the same source text is mapped to multiple different
  /// target locations.
  ///
  /// This also re-indents the input strings with [_reindent].
  void _expectSourceMap(
    String sass,
    String scss,
    String css, {
    Importer? importer,
    OutputStyle? style,
  }) {
    _expectSassSourceMap(sass, css, importer: importer, style: style);
    _expectScssSourceMap(scss, css, importer: importer, style: style);
  }
  
  /// Like [_expectSourceMap], but with only SCSS source.
  void _expectScssSourceMap(
    String scss,
    String css, {
    Importer? importer,
    OutputStyle? style,
  }) {
    var (scssText, scssLocations) = _extractLocations(_reindent(scss));
    var (cssText, cssLocations) = _extractLocations(_reindent(css));
  
    late SingleMapping scssMap;
    var scssOutput = compileString(
      scssText,
      sourceMap: (map) => scssMap = map,
      importer: importer,
      style: style,
    );
    expect(scssOutput, equals(cssText));
    _expectMapMatches(
      scssMap,
      scssText,
      cssText,
      _pairsToMap(scssLocations),
      cssLocations,
    );
  }
  
  /// Like [_expectSourceMap], but with only indented source.
  void _expectSassSourceMap(
    String sass,
    String css, {
    Importer? importer,
    OutputStyle? style,
  }) {
    var (sassText, sassLocations) = _extractLocations(_reindent(sass));
    var (cssText, cssLocations) = _extractLocations(_reindent(css));
  
    late SingleMapping sassMap;
    var sassOutput = compileString(
      sassText,
      indented: true,
      sourceMap: (map) => sassMap = map,
      importer: importer,
      style: style,
    );
    expect(sassOutput, equals(cssText));
    _expectMapMatches(
      sassMap,
      sassText,
      cssText,
      _pairsToMap(sassLocations),
      cssLocations,
    );
  }
  
  /// Returns [string] with leading whitespace stripped from each line so that the
  /// least-indented line has zero indentation.
  String _reindent(String string) {
    var lines = trimAsciiRight(string).split("\n");
    var minIndent = lines
        .where((line) => trimAscii(line).isNotEmpty)
        .map((line) => line.length - trimAsciiLeft(line).length)
        .reduce((length1, length2) => length1 < length2 ? length1 : length2);
    return lines
        .map((line) => trimAscii(line).isEmpty ? "" : line.substring(minIndent))
        .join("\n");
  }
  
  /// Parses and removes the location annotations from [text].
  (String, List<(String, SourceLocation)>) _extractLocations(String text) {
    var scanner = StringScanner(text);
    var buffer = StringBuffer();
    var locations = <(String, SourceLocation)>[];
  
    var offset = 0;
    var line = 0;
    var column = 0;
    while (!scanner.isDone) {
      if (scanner.matches(RegExp(r"{{[^{]"))) {
        scanner.expect("{{");
        var start = scanner.position;
        while (!scanner.scan("}}")) {
          scanner.readChar();
        }
        locations.add((
          scanner.substring(start, scanner.position - 2),
          SourceLocation(offset, line: line, column: column),
        ));
      } else if (scanner.scanChar($lf)) {
        offset++;
        line++;
        column = 0;
        buffer.writeln();
      } else {
        buffer.writeCharCode(scanner.readChar());
        offset++;
        column++;
      }
    }
  
    return (buffer.toString(), locations);
  }
  
  /// Converts a list of pairs to a map, asserting that each key appears only
  /// once.
  Map<K, V> _pairsToMap<K, V>(Iterable<(K, V)> pairs) {
    var map = <K, V>{};
    for (var (key, value) in pairs) {
      expect(map, isNot(contains(key)));
      map[key] = value;
    }
    return map;
  }
  
  /// Asserts that the entries in [map] match the map given by [sourceLocations]
  /// and [targetLocations].
  void _expectMapMatches(
    SingleMapping map,
    String sourceText,
    String targetText,
    Map<String, SourceLocation> sourceLocations,
    List<(String, SourceLocation)> targetLocations,
  ) {
    expect(
      sourceLocations.keys,
      equals({for (var (name, _) in targetLocations) name}),
    );
  
    String actualMap() =>
        "\nActual map:\n\n" + _mapToString(map, sourceText, targetText) + "\n";
  
    var entryIter = _entriesForMap(map).iterator;
    for (var (name, expectedTarget) in targetLocations) {
      var expectedSource = sourceLocations[name]!;
  
      if (!entryIter.moveNext()) {
        fail(
          'Missing mapping "$name", expected '
                  '${_mapping(expectedSource, expectedTarget)}.\n' +
              actualMap(),
        );
      }
  
      var entry = entryIter.current;
      if (expectedSource.line != entry.source.line ||
          expectedSource.column != entry.source.column ||
          expectedTarget.line != entry.target.line ||
          expectedTarget.column != entry.target.column) {
        fail(
          'Mapping "$name" was ${_mapping(entry.source, entry.target)}, '
                  'expected ${_mapping(expectedSource, expectedTarget)}.\n' +
              actualMap(),
        );
      }
    }
  
    expect(
      entryIter.moveNext(),
      isFalse,
      reason: 'Expected no more mappings.\n' + actualMap(),
    );
  }
  
  /// Converts a [map] back into [Entry]s.
  Iterable<Entry> _entriesForMap(SingleMapping map) sync* {
    for (var lineEntry in map.lines) {
      for (var entry in lineEntry.entries) {
        yield Entry(
          SourceLocation(0, line: entry.sourceLine, column: entry.sourceColumn),
          SourceLocation(0, line: lineEntry.line, column: entry.column),
          null,
        );
      }
    }
  }
  
  /// Returns a terse human-readable string for a mapping from a source
  /// [SourceLocation] to a target [SourceLocation].
  String _mapping(SourceLocation source, SourceLocation target) =>
      "${_location(source)} to ${_location(target)}";
  
  /// Returns a terse human-readable string for a [SourceLocation].
  String _location(SourceLocation location) =>
      "${location.line}:${location.column}";
  
  /// Returns a human-readable string representation of [map] between [sourceText]
  /// and [targetText].
  String _mapToString(SingleMapping map, String sourceText, String targetText) {
    var entries = _entriesForMap(map);
    var entriesInSourceOrder = entries.toList()
      ..sort((entry1, entry2) => entry1.source.compareTo(entry2.source));
  
    // A map from lines and columns in [sourceText] to the names of the entries
    // with those source locations.
    var entryNames = <(int, int), String>{};
    var i = 0;
    for (var entry in entriesInSourceOrder) {
      entryNames.putIfAbsent((
        entry.source.line,
        entry.source.column,
      ), () => (++i).toString());
    }
  
    var sourceScanner = LineScanner(sourceText);
    var sourceBuffer = StringBuffer();
    while (!sourceScanner.isDone) {
      var name = entryNames[(sourceScanner.line, sourceScanner.column)];
      if (name != null) sourceBuffer.write("{{$name}}");
      sourceBuffer.writeCharCode(sourceScanner.readChar());
    }
  
    var targetScanner = LineScanner(targetText);
    var targetBuffer = StringBuffer();
    var entryIter = entries.iterator..moveNext();
    while (!targetScanner.isDone) {
      var entry = entryIter.current;
      if (targetScanner.line == entry.target.line &&
          targetScanner.column == entry.target.column) {
        var name = entryNames[(entry.source.line, entry.source.column)];
        targetBuffer.write("{{$name}}");
        if (!entryIter.moveNext()) {
          targetBuffer.write(targetScanner.rest);
          break;
        }
      }
  
      targetBuffer.writeCharCode(targetScanner.readChar());
    }
  
    return sourceBuffer.toString() +
        "\n\n" +
        "v" * 50 +
        "\n\n" +
        targetBuffer.toString();
  }
  