content: // Copyright 2019 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.
  
  import 'package:test/test.dart';
  
  import 'package:sass_embedded/src/embedded_sass.pb.dart';
  import 'package:sass_embedded/src/utils.dart';
  
  import 'embedded_process.dart';
  import 'utils.dart';
  
  final _true = Value()..singleton = SingletonValue.TRUE;
  final _false = Value()..singleton = SingletonValue.FALSE;
  final _null = Value()..singleton = SingletonValue.NULL;
  
  late EmbeddedProcess _process;
  
  void main() {
    setUp(() async {
      _process = await EmbeddedProcess.start();
    });
  
    group("emits a compile failure for a custom function with a signature", () {
      test("that's empty", () async {
        _process.inbound.add(compileString("a {b: c}", functions: [r""]));
        await _expectFunctionError(
            _process, r'Invalid signature "": Expected identifier.');
        await _process.kill();
      });
  
      test("that's just a name", () async {
        _process.inbound.add(compileString("a {b: c}", functions: [r"foo"]));
        await _expectFunctionError(
            _process, r'Invalid signature "foo": expected "(".');
        await _process.kill();
      });
  
      test("without a closing paren", () async {
        _process.inbound.add(compileString("a {b: c}", functions: [r"foo($bar"]));
        await _expectFunctionError(
            _process, r'Invalid signature "foo($bar": expected ")".');
        await _process.kill();
      });
  
      test("with text after the closing paren", () async {
        _process.inbound.add(compileString("a {b: c}", functions: [r"foo() "]));
        await _expectFunctionError(
            _process, r'Invalid signature "foo() ": expected no more input.');
        await _process.kill();
      });
  
      test("with invalid arguments", () async {
        _process.inbound.add(compileString("a {b: c}", functions: [r"foo($)"]));
        await _expectFunctionError(
            _process, r'Invalid signature "foo($)": Expected identifier.');
        await _process.kill();
      });
    });
  
    group("includes in FunctionCallRequest", () {
      var compilationId = 1234;
      late OutboundMessage_FunctionCallRequest request;
      setUp(() async {
        _process.inbound.add(compileString("a {b: foo()}",
            id: compilationId, functions: ["foo()"]));
        request = getFunctionCallRequest(await _process.outbound.next);
      });
  
      test("the same compilationId as the compilation", () async {
        expect(request.compilationId, equals(compilationId));
        await _process.kill();
      });
  
      test("the function name", () async {
        expect(request.name, equals("foo"));
        await _process.kill();
      });
  
      group("arguments", () {
        test("that are empty", () async {
          _process.inbound
              .add(compileString("a {b: foo()}", functions: ["foo()"]));
          var request = getFunctionCallRequest(await _process.outbound.next);
          expect(request.arguments, isEmpty);
          await _process.kill();
        });
  
        test("by position", () async {
          _process.inbound.add(compileString("a {b: foo(true, null, false)}",
              functions: [r"foo($arg1, $arg2, $arg3)"]));
          var request = getFunctionCallRequest(await _process.outbound.next);
          expect(request.arguments, equals([_true, _null, _false]));
          await _process.kill();
        });
  
        test("by name", () async {
          _process.inbound.add(compileString(
              r"a {b: foo($arg3: true, $arg1: null, $arg2: false)}",
              functions: [r"foo($arg1, $arg2, $arg3)"]));
          var request = getFunctionCallRequest(await _process.outbound.next);
          expect(request.arguments, equals([_null, _false, _true]));
          await _process.kill();
        });
  
        test("by position and name", () async {
          _process.inbound.add(compileString(
              r"a {b: foo(true, $arg3: null, $arg2: false)}",
              functions: [r"foo($arg1, $arg2, $arg3)"]));
          var request = getFunctionCallRequest(await _process.outbound.next);
          expect(request.arguments, equals([_true, _false, _null]));
          await _process.kill();
        });
  
        test("from defaults", () async {
          _process.inbound.add(compileString(r"a {b: foo(1, $arg3: 2)}",
              functions: [r"foo($arg1: null, $arg2: true, $arg3: false)"]));
          var request = getFunctionCallRequest(await _process.outbound.next);
          expect(
              request.arguments,
              equals([
                Value()..number = (Value_Number()..value = 1.0),
                _true,
                Value()..number = (Value_Number()..value = 2.0)
              ]));
          await _process.kill();
        });
  
        group("from argument lists", () {
          test("with no named arguments", () async {
            _process.inbound.add(compileString("a {b: foo(true, false, null)}",
                functions: [r"foo($arg, $args...)"]));
            var request = getFunctionCallRequest(await _process.outbound.next);
  
            expect(
                request.arguments,
                equals([
                  _true,
                  Value()
                    ..argumentList = (Value_ArgumentList()
                      ..id = 1
                      ..separator = ListSeparator.COMMA
                      ..contents.addAll([_false, _null]))
                ]));
            await _process.kill();
          });
  
          test("with named arguments", () async {
            _process.inbound.add(compileString(r"a {b: foo(true, $arg: false)}",
                functions: [r"foo($args...)"]));
            var request = getFunctionCallRequest(await _process.outbound.next);
  
            expect(
                request.arguments,
                equals([
                  Value()
                    ..argumentList = (Value_ArgumentList()
                      ..id = 1
                      ..separator = ListSeparator.COMMA
                      ..contents.addAll([_true])
                      ..keywords.addAll({"arg": _false}))
                ]));
            await _process.kill();
          });
  
          test("throws if named arguments are unused", () async {
            _process.inbound.add(compileString(r"a {b: foo($arg: false)}",
                functions: [r"foo($args...)"]));
            var request = getFunctionCallRequest(await _process.outbound.next);
  
            _process.inbound.add(InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..success = _true));
  
            var failure = getCompileFailure(await _process.outbound.next);
            expect(failure.message, equals(r"No argument named $arg."));
            await _process.kill();
          });
  
          test("doesn't throw if named arguments are used", () async {
            _process.inbound.add(compileString(r"a {b: foo($arg: false)}",
                functions: [r"foo($args...)"]));
            var request = getFunctionCallRequest(await _process.outbound.next);
  
            _process.inbound.add(InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..accessedArgumentLists
                    .add(request.arguments.first.argumentList.id)
                ..success = _true));
  
            await expectLater(_process.outbound,
                emits(isSuccess(equals("a {\n  b: true;\n}"))));
            await _process.kill();
          });
        });
      });
    });
  
    test("returns the result as a SassScript value", () async {
      _process.inbound
          .add(compileString("a {b: foo() + 2px}", functions: [r"foo()"]));
      var request = getFunctionCallRequest(await _process.outbound.next);
  
      _process.inbound.add(InboundMessage()
        ..functionCallResponse = (InboundMessage_FunctionCallResponse()
          ..id = request.id
          ..success = (Value()
            ..number = (Value_Number()
              ..value = 1
              ..numerators.add("px")))));
  
      await expectLater(
          _process.outbound, emits(isSuccess(equals("a {\n  b: 3px;\n}"))));
      await _process.kill();
    });
  
    group("calls a first-class function", () {
      test("defined in the compiler and passed to and from the host", () async {
        _process.inbound.add(compileString(r"""
          @use "sass:math";
          @use "sass:meta";
  
          a {b: call(foo(meta.get-function("abs", $module: "math")), -1)}
        """, functions: [r"foo($arg)"]));
  
        var request = getFunctionCallRequest(await _process.outbound.next);
        var value = request.arguments.single;
        expect(value.hasCompilerFunction(), isTrue);
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = value));
  
        await expectLater(
            _process.outbound, emits(isSuccess(equals("a {\n  b: 1;\n}"))));
        await _process.kill();
      });
  
      test("defined in the host", () async {
        var compilationId = 1234;
        _process.inbound.add(compileString("a {b: call(foo(), true)}",
            id: compilationId, functions: [r"foo()"]));
  
        var hostFunctionId = 5678;
        var request = getFunctionCallRequest(await _process.outbound.next);
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = (Value()
              ..hostFunction = (Value_HostFunction()
                ..id = hostFunctionId
                ..signature = r"bar($arg)"))));
  
        request = getFunctionCallRequest(await _process.outbound.next);
        expect(request.compilationId, equals(compilationId));
        expect(request.functionId, equals(hostFunctionId));
        expect(request.arguments, equals([_true]));
  
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = _false));
  
        await expectLater(
            _process.outbound, emits(isSuccess(equals("a {\n  b: false;\n}"))));
        await _process.kill();
      });
  
      test("defined in the host and passed to and from the host", () async {
        var compilationId = 1234;
        _process.inbound.add(compileString(
            r"""
              $function: get-host-function();
              $function: round-trip($function);
              a {b: call($function, true)}
            """,
            id: compilationId,
            functions: [r"get-host-function()", r"round-trip($function)"]));
  
        var hostFunctionId = 5678;
        var request = getFunctionCallRequest(await _process.outbound.next);
        expect(request.name, equals("get-host-function"));
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = (Value()
              ..hostFunction = (Value_HostFunction()
                ..id = hostFunctionId
                ..signature = r"bar($arg)"))));
  
        request = getFunctionCallRequest(await _process.outbound.next);
        expect(request.name, equals("round-trip"));
        var value = request.arguments.single;
        expect(value.hasCompilerFunction(), isTrue);
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = value));
  
        request = getFunctionCallRequest(await _process.outbound.next);
        expect(request.compilationId, equals(compilationId));
        expect(request.functionId, equals(hostFunctionId));
        expect(request.arguments, equals([_true]));
  
        _process.inbound.add(InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = _false));
  
        await expectLater(
            _process.outbound, emits(isSuccess(equals("a {\n  b: false;\n}"))));
        await _process.kill();
      });
    });
  
    group("serializes to protocol buffers", () {
      group("a string that's", () {
        group("quoted", () {
          test("and empty", () async {
            var value = (await _protofy('""')).string;
            expect(value.text, isEmpty);
            expect(value.quoted, isTrue);
          });
  
          test("and non-empty", () async {
            var value = (await _protofy('"foo bar"')).string;
            expect(value.text, equals("foo bar"));
            expect(value.quoted, isTrue);
          });
        });
  
        group("unquoted", () {
          test("and empty", () async {
            var value = (await _protofy('unquote("")')).string;
            expect(value.text, isEmpty);
            expect(value.quoted, isFalse);
          });
  
          test("and non-empty", () async {
            var value = (await _protofy('"foo bar"')).string;
            expect(value.text, equals("foo bar"));
            expect(value.quoted, isTrue);
          });
        });
      });
  
      group("a number", () {
        group("that's unitless", () {
          test("and an integer", () async {
            var value = (await _protofy('1')).number;
            expect(value.value, equals(1.0));
            expect(value.numerators, isEmpty);
            expect(value.denominators, isEmpty);
          });
  
          test("and a float", () async {
            var value = (await _protofy('1.5')).number;
            expect(value.value, equals(1.5));
            expect(value.numerators, isEmpty);
            expect(value.denominators, isEmpty);
          });
        });
  
        test("with one numerator", () async {
          var value = (await _protofy('1em')).number;
          expect(value.value, equals(1.0));
          expect(value.numerators, ["em"]);
          expect(value.denominators, isEmpty);
        });
  
        test("with multiple numerators", () async {
          var value = (await _protofy('1em * 1px * 1foo')).number;
          expect(value.value, equals(1.0));
          expect(value.numerators, unorderedEquals(["em", "px", "foo"]));
          expect(value.denominators, isEmpty);
        });
  
        test("with one denominator", () async {
          var value = (await _protofy('math.div(1,1em)')).number;
          expect(value.value, equals(1.0));
          expect(value.numerators, isEmpty);
          expect(value.denominators, ["em"]);
        });
  
        test("with multiple denominators", () async {
          var value =
              (await _protofy('math.div(math.div(math.div(1, 1em), 1px), 1foo)'))
                  .number;
          expect(value.value, equals(1.0));
          expect(value.numerators, isEmpty);
          expect(value.denominators, unorderedEquals(["em", "px", "foo"]));
        });
  
        test("with numerators and denominators", () async {
          var value =
              (await _protofy('1em * math.div(math.div(1px, 1s), 1foo)')).number;
          expect(value.value, equals(1.0));
          expect(value.numerators, unorderedEquals(["em", "px"]));
          expect(value.denominators, unorderedEquals(["s", "foo"]));
        });
      });
  
      group("a color that's", () {
        group("rgb", () {
          group("without alpha:", () {
            test("black", () async {
              expect(await _protofy('#000'), _rgb(0, 0, 0, 1.0));
            });
  
            test("white", () async {
              expect(await _protofy('#fff'), equals(_rgb(255, 255, 255, 1.0)));
            });
  
            test("in the middle", () async {
              expect(await _protofy('#abc'), equals(_rgb(0xaa, 0xbb, 0xcc, 1.0)));
            });
          });
  
          group("with alpha", () {
            test("0", () async {
              expect(await _protofy('rgb(10, 20, 30, 0)'),
                  equals(_rgb(10, 20, 30, 0.0)));
            });
  
            test("1", () async {
              expect(await _protofy('rgb(10, 20, 30, 1)'),
                  equals(_rgb(10, 20, 30, 1.0)));
            });
  
            test("between 0 and 1", () async {
              expect(await _protofy('rgb(10, 20, 30, 0.123)'),
                  equals(_rgb(10, 20, 30, 0.123)));
            });
          });
        });
  
        group("hsl", () {
          group("without alpha:", () {
            group("hue", () {
              test("0", () async {
                expect(await _protofy('hsl(0, 50%, 50%)'), _hsl(0, 50, 50, 1.0));
              });
  
              test("360", () async {
                expect(
                    await _protofy('hsl(360, 50%, 50%)'), _hsl(0, 50, 50, 1.0));
              });
  
              test("below 0", () async {
                expect(await _protofy('hsl(-100, 50%, 50%)'),
                    _hsl(260, 50, 50, 1.0));
              });
  
              test("between 0 and 360", () async {
                expect(
                    await _protofy('hsl(100, 50%, 50%)'), _hsl(100, 50, 50, 1.0));
              });
  
              test("above 360", () async {
                expect(
                    await _protofy('hsl(560, 50%, 50%)'), _hsl(200, 50, 50, 1.0));
              });
            });
  
            group("saturation", () {
              test("0", () async {
                expect(await _protofy('hsl(0, 0%, 50%)'), _hsl(0, 0, 50, 1.0));
              });
  
              test("100", () async {
                expect(
                    await _protofy('hsl(0, 100%, 50%)'), _hsl(0, 100, 50, 1.0));
              });
  
              test("in the middle", () async {
                expect(await _protofy('hsl(0, 42%, 50%)'), _hsl(0, 42, 50, 1.0));
              });
            });
  
            group("lightness", () {
              test("0", () async {
                expect(await _protofy('hsl(0, 50%, 0%)'), _hsl(0, 50, 0, 1.0));
              });
  
              test("100", () async {
                expect(
                    await _protofy('hsl(0, 50%, 100%)'), _hsl(0, 50, 100, 1.0));
              });
  
              test("in the middle", () async {
                expect(await _protofy('hsl(0, 50%, 42%)'), _hsl(0, 50, 42, 1.0));
              });
            });
          });
  
          group("with alpha", () {
            test("0", () async {
              expect(await _protofy('hsl(10, 20%, 30%, 0)'),
                  equals(_hsl(10, 20, 30, 0.0)));
            });
  
            test("1", () async {
              expect(await _protofy('hsl(10, 20%, 30%, 1)'),
                  equals(_hsl(10, 20, 30, 1.0)));
            });
  
            test("between 0 and 1", () async {
              expect(await _protofy('hsl(10, 20%, 30%, 0.123)'),
                  equals(_hsl(10, 20, 30, 0.123)));
            });
          });
        });
      });
  
      group("a list", () {
        group("with no elements", () {
          group("with brackets", () {
            test("with unknown separator", () async {
              var list = (await _protofy("[]")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.UNDECIDED));
            });
  
            test("with a comma separator", () async {
              var list =
                  (await _protofy(r"list.join([], [], $separator: comma)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list =
                  (await _protofy(r"list.join([], [], $separator: space)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
  
            test("with a slash separator", () async {
              var list =
                  (await _protofy(r"list.join([], [], $separator: slash)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.SLASH));
            });
          });
  
          group("without brackets", () {
            test("with unknown separator", () async {
              var list = (await _protofy("()")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.UNDECIDED));
            });
  
            test("with a comma separator", () async {
              var list =
                  (await _protofy(r"list.join((), (), $separator: comma)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list =
                  (await _protofy(r"list.join((), (), $separator: space)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
  
            test("with a slash separator", () async {
              var list =
                  (await _protofy(r"list.join((), (), $separator: slash)")).list;
              expect(list.contents, isEmpty);
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SLASH));
            });
          });
        });
  
        group("with one element", () {
          group("with brackets", () {
            test("with unknown separator", () async {
              var list = (await _protofy("[true]")).list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.UNDECIDED));
            });
  
            test("with a comma separator", () async {
              var list = (await _protofy(r"[true,]")).list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list =
                  (await _protofy(r"list.join([true], [], $separator: space)"))
                      .list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
  
            test("with a slash separator", () async {
              var list =
                  (await _protofy(r"list.join([true], [], $separator: slash)"))
                      .list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.SLASH));
            });
          });
  
          group("without brackets", () {
            test("with a comma separator", () async {
              var list = (await _protofy(r"(true,)")).list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list =
                  (await _protofy(r"list.join(true, (), $separator: space)"))
                      .list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
  
            test("with a slash separator", () async {
              var list =
                  (await _protofy(r"list.join(true, (), $separator: slash)"))
                      .list;
              expect(list.contents, equals([_true]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SLASH));
            });
          });
        });
  
        group("with multiple elements", () {
          group("with brackets", () {
            test("with a comma separator", () async {
              var list = (await _protofy(r"[true, null, false]")).list;
              expect(list.contents, equals([_true, _null, _false]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list = (await _protofy(r"[true null false]")).list;
              expect(list.contents, equals([_true, _null, _false]));
              expect(list.hasBrackets, isTrue);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
          });
  
          group("without brackets", () {
            test("with a comma separator", () async {
              var list = (await _protofy(r"true, null, false")).list;
              expect(list.contents, equals([_true, _null, _false]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.COMMA));
            });
  
            test("with a space separator", () async {
              var list = (await _protofy(r"true null false")).list;
              expect(list.contents, equals([_true, _null, _false]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SPACE));
            });
  
            test("with a slash separator", () async {
              var list = (await _protofy(r"list.slash(true, null, false)")).list;
              expect(list.contents, equals([_true, _null, _false]));
              expect(list.hasBrackets, isFalse);
              expect(list.separator, equals(ListSeparator.SLASH));
            });
          });
        });
      });
  
      group("an argument list", () {
        test("that's empty", () async {
          var list = (await _protofy(r"capture-args()")).argumentList;
          expect(list.contents, isEmpty);
          expect(list.keywords, isEmpty);
          expect(list.separator, equals(ListSeparator.COMMA));
        });
  
        test("with arguments", () async {
          var list =
              (await _protofy(r"capture-args(true, null, false)")).argumentList;
          expect(list.contents, [_true, _null, _false]);
          expect(list.keywords, isEmpty);
          expect(list.separator, equals(ListSeparator.COMMA));
        });
  
        test("with a space separator", () async {
          var list =
              (await _protofy(r"capture-args(true null false...)")).argumentList;
          expect(list.contents, [_true, _null, _false]);
          expect(list.keywords, isEmpty);
          expect(list.separator, equals(ListSeparator.SPACE));
        });
  
        test("with a slash separator", () async {
          var list =
              (await _protofy(r"capture-args(list.slash(true, null, false)...)"))
                  .argumentList;
          expect(list.contents, [_true, _null, _false]);
          expect(list.keywords, isEmpty);
          expect(list.separator, equals(ListSeparator.SLASH));
        });
  
        test("with keywords", () async {
          var list = (await _protofy(r"capture-args($arg1: true, $arg2: false)"))
              .argumentList;
          expect(list.contents, isEmpty);
          expect(list.keywords, equals({"arg1": _true, "arg2": _false}));
          expect(list.separator, equals(ListSeparator.COMMA));
        });
      });
  
      group("a map", () {
        test("with no elements", () async {
          expect((await _protofy("map.remove((1: 2), 1)")).map.entries, isEmpty);
        });
  
        test("with one element", () async {
          expect(
              (await _protofy("(true: false)")).map.entries,
              equals([
                Value_Map_Entry()
                  ..key = _true
                  ..value = _false
              ]));
        });
  
        test("with multiple elements", () async {
          expect(
              (await _protofy("(true: false, 1: 2, a: b)")).map.entries,
              equals([
                Value_Map_Entry()
                  ..key = _true
                  ..value = _false,
                Value_Map_Entry()
                  ..key = (Value()..number = (Value_Number()..value = 1.0))
                  ..value = (Value()..number = (Value_Number()..value = 2.0)),
                Value_Map_Entry()
                  ..key = (Value()
                    ..string = (Value_String()
                      ..text = "a"
                      ..quoted = false))
                  ..value = (Value()
                    ..string = (Value_String()
                      ..text = "b"
                      ..quoted = false))
              ]));
        });
      });
  
      group("a calculation", () {
        test("with a string argument", () async {
          expect(
              (await _protofy("calc(var(--foo))")).calculation,
              equals(Value_Calculation()
                ..name = "calc"
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..string = "var(--foo)")));
        });
  
        test("with an interpolation argument", () async {
          expect(
              (await _protofy("calc(#{var(--foo)})")).calculation,
              equals(Value_Calculation()
                ..name = "calc"
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..interpolation = "var(--foo)")));
        });
  
        test("with number arguments", () async {
          expect(
              (await _protofy("clamp(1%, 2px, 3em)")).calculation,
              equals(Value_Calculation()
                ..name = "clamp"
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..number = (Value_Number()
                    ..value = 1.0
                    ..numerators.add("%")))
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..number = (Value_Number()
                    ..value = 2.0
                    ..numerators.add("px")))
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..number = (Value_Number()
                    ..value = 3.0
                    ..numerators.add("em")))));
        });
  
        test("with a calculation argument", () async {
          expect(
              (await _protofy("min(max(1%, 2px), 3em)")).calculation,
              equals(Value_Calculation()
                ..name = "min"
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..calculation = (Value_Calculation()
                    ..name = "max"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 1.0
                        ..numerators.add("%")))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 2.0
                        ..numerators.add("px")))))
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..number = (Value_Number()
                    ..value = 3.0
                    ..numerators.add("em")))));
        });
  
        test("with an operation", () async {
          expect(
              (await _protofy("calc(1% + 2px)")).calculation,
              equals(Value_Calculation()
                ..name = "calc"
                ..arguments.add(Value_Calculation_CalculationValue()
                  ..operation = (Value_Calculation_CalculationOperation()
                    ..operator = CalculationOperator.PLUS
                    ..left = (Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 1.0
                        ..numerators.add("%")))
                    ..right = (Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 2.0
                        ..numerators.add("px")))))));
        });
      });
  
      test("true", () async {
        expect((await _protofy("true")), equals(_true));
      });
  
      test("false", () async {
        expect((await _protofy("false")), equals(_false));
      });
  
      test("true", () async {
        expect((await _protofy("null")), equals(_null));
      });
    });
  
    group("deserializes from protocol buffer", () {
      group("a string that's", () {
        group("quoted", () {
          test("and empty", () async {
            expect(
                await _deprotofy(Value()
                  ..string = (Value_String()
                    ..text = ""
                    ..quoted = true)),
                '""');
          });
  
          test("and non-empty", () async {
            expect(
                await _deprotofy(Value()
                  ..string = (Value_String()
                    ..text = "foo bar"
                    ..quoted = true)),
                '"foo bar"');
          });
        });
  
        group("unquoted", () {
          test("and empty", () async {
            // We can't use [_deprotofy] here because a property with an empty
            // value won't render at all.
            await _assertRoundTrips(Value()
              ..string = (Value_String()
                ..text = ""
                ..quoted = false));
          });
  
          test("and non-empty", () async {
            expect(
                await _deprotofy(Value()
                  ..string = (Value_String()
                    ..text = "foo bar"
                    ..quoted = false)),
                "foo bar");
          });
        });
      });
  
      group("a number", () {
        group("that's unitless", () {
          test("and an integer", () async {
            expect(
                await _deprotofy(Value()..number = (Value_Number()..value = 1.0)),
                "1");
          });
  
          test("and a float", () async {
            expect(
                await _deprotofy(Value()..number = (Value_Number()..value = 1.5)),
                "1.5");
          });
        });
  
        test("with one numerator", () async {
          expect(
              await _deprotofy(Value()
                ..number = (Value_Number()
                  ..value = 1
                  ..numerators.add("em"))),
              "1em");
        });
  
        test("with multiple numerators", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..number = (Value_Number()
                      ..value = 1
                      ..numerators.addAll(["em", "px", "foo"])),
                  inspect: true),
              "1em*px*foo");
        });
  
        test("with one denominator", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..number = (Value_Number()
                      ..value = 1
                      ..denominators.add("em")),
                  inspect: true),
              "1em^-1");
        });
  
        test("with multiple denominators", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..number = (Value_Number()
                      ..value = 1
                      ..denominators.addAll(["em", "px", "foo"])),
                  inspect: true),
              "1(em*px*foo)^-1");
        });
  
        test("with numerators and denominators", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..number = (Value_Number()
                      ..value = 1
                      ..numerators.addAll(["em", "px"])
                      ..denominators.addAll(["s", "foo"])),
                  inspect: true),
              "1em*px/s*foo");
        });
      });
  
      group("a color that's", () {
        group("rgb", () {
          group("without alpha:", () {
            test("black", () async {
              expect(await _deprotofy(_rgb(0, 0, 0, 1.0)), equals('black'));
            });
  
            test("white", () async {
              expect(await _deprotofy(_rgb(255, 255, 255, 1.0)), equals('white'));
            });
  
            test("in the middle", () async {
              expect(await _deprotofy(_rgb(0xaa, 0xbb, 0xcc, 1.0)),
                  equals('#aabbcc'));
            });
          });
  
          group("with alpha", () {
            test("0", () async {
              expect(await _deprotofy(_rgb(10, 20, 30, 0.0)),
                  equals('rgba(10, 20, 30, 0)'));
            });
  
            test("between 0 and 1", () async {
              expect(await _deprotofy(_rgb(10, 20, 30, 0.123)),
                  equals('rgba(10, 20, 30, 0.123)'));
            });
          });
        });
  
        group("hsl", () {
          group("without alpha:", () {
            group("hue", () {
              test("0", () async {
                expect(await _deprotofy(_hsl(0, 50, 50, 1.0)), "#bf4040");
              });
  
              test("360", () async {
                expect(await _deprotofy(_hsl(360, 50, 50, 1.0)), "#bf4040");
              });
  
              test("below 0", () async {
                expect(await _deprotofy(_hsl(-100, 50, 50, 1.0)), "#6a40bf");
              });
  
              test("between 0 and 360", () async {
                expect(await _deprotofy(_hsl(100, 50, 50, 1.0)), "#6abf40");
              });
  
              test("above 360", () async {
                expect(await _deprotofy(_hsl(560, 50, 50, 1.0)), "#4095bf");
              });
            });
  
            group("saturation", () {
              test("0", () async {
                expect(await _deprotofy(_hsl(0, 0, 50, 1.0)), "gray");
              });
  
              test("100", () async {
                expect(await _deprotofy(_hsl(0, 100, 50, 1.0)), "red");
              });
  
              test("in the middle", () async {
                expect(await _deprotofy(_hsl(0, 42, 50, 1.0)), "#b54a4a");
              });
            });
  
            group("lightness", () {
              test("0", () async {
                expect(await _deprotofy(_hsl(0, 50, 0, 1.0)), "black");
              });
  
              test("100", () async {
                expect(await _deprotofy(_hsl(0, 50, 100, 1.0)), "white");
              });
  
              test("in the middle", () async {
                expect(await _deprotofy(_hsl(0, 50, 42, 1.0)), "#a13636");
              });
            });
          });
  
          group("with alpha", () {
            test("0", () async {
              expect(
                  await _deprotofy(_hsl(10, 20, 30, 0.0)), "rgba(92, 66, 61, 0)");
            });
  
            test("between 0 and 1", () async {
              expect(await _deprotofy(_hsl(10, 20, 30, 0.123)),
                  "rgba(92, 66, 61, 0.123)");
            });
          });
        });
      });
  
      group("a list", () {
        group("with no elements", () {
          group("with brackets", () {
            group("with unknown separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = true
                      ..separator = ListSeparator.UNDECIDED),
                  "[]");
            });
  
            group("with a comma separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = true
                      ..separator = ListSeparator.COMMA),
                  "[]");
            });
  
            group("with a space separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = true
                      ..separator = ListSeparator.SPACE),
                  "[]");
            });
  
            group("with a slash separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = true
                      ..separator = ListSeparator.SLASH),
                  "[]");
            });
          });
  
          group("without brackets", () {
            group("with unknown separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = false
                      ..separator = ListSeparator.UNDECIDED),
                  "()",
                  inspect: true);
            });
  
            group("with a comma separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = false
                      ..separator = ListSeparator.COMMA),
                  "()",
                  inspect: true);
            });
  
            group("with a space separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = false
                      ..separator = ListSeparator.SPACE),
                  "()",
                  inspect: true);
            });
  
            group("with a slash separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..hasBrackets = false
                      ..separator = ListSeparator.SLASH),
                  "()",
                  inspect: true);
            });
          });
        });
  
        group("with one element", () {
          group("with brackets", () {
            group("with unknown separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = true
                      ..separator = ListSeparator.UNDECIDED),
                  "[true]");
            });
  
            test("with a comma separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.add(_true)
                          ..hasBrackets = true
                          ..separator = ListSeparator.COMMA),
                      inspect: true),
                  "[true,]");
            });
  
            group("with a space separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = true
                      ..separator = ListSeparator.SPACE),
                  "[true]");
            });
  
            group("with a slash separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = true
                      ..separator = ListSeparator.SLASH),
                  "[true]");
            });
          });
  
          group("without brackets", () {
            group("with unknown separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = false
                      ..separator = ListSeparator.UNDECIDED),
                  "true");
            });
  
            test("with a comma separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.add(_true)
                          ..hasBrackets = false
                          ..separator = ListSeparator.COMMA),
                      inspect: true),
                  "(true,)");
            });
  
            group("with a space separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = false
                      ..separator = ListSeparator.SPACE),
                  "true");
            });
  
            group("with a slash separator", () {
              _testSerializationAndRoundTrip(
                  Value()
                    ..list = (Value_List()
                      ..contents.add(_true)
                      ..hasBrackets = false
                      ..separator = ListSeparator.SLASH),
                  "true");
            });
          });
        });
  
        group("with multiple elements", () {
          group("with brackets", () {
            test("with a comma separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = true
                          ..separator = ListSeparator.COMMA),
                      inspect: true),
                  "[true, null, false]");
            });
  
            test("with a space separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = true
                          ..separator = ListSeparator.SPACE),
                      inspect: true),
                  "[true null false]");
            });
  
            test("with a slash separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = true
                          ..separator = ListSeparator.SLASH),
                      inspect: true),
                  "[true / null / false]");
            });
          });
  
          group("without brackets", () {
            test("with a comma separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = false
                          ..separator = ListSeparator.COMMA),
                      inspect: true),
                  "true, null, false");
            });
  
            test("with a space separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = false
                          ..separator = ListSeparator.SPACE),
                      inspect: true),
                  "true null false");
            });
  
            test("with a slash separator", () async {
              expect(
                  await _deprotofy(
                      Value()
                        ..list = (Value_List()
                          ..contents.addAll([_true, _null, _false])
                          ..hasBrackets = false
                          ..separator = ListSeparator.SLASH),
                      inspect: true),
                  "true / null / false");
            });
          });
        });
      });
  
      group("an argument list", () {
        test("with no elements", () async {
          expect(
              await _roundTrip(Value()
                ..argumentList =
                    (Value_ArgumentList()..separator = ListSeparator.UNDECIDED)),
              equals(Value()
                ..argumentList = (Value_ArgumentList()
                  ..id = 1
                  ..separator = ListSeparator.UNDECIDED)));
        });
  
        test("with comma separator", () async {
          expect(
              await _roundTrip(Value()
                ..argumentList = (Value_ArgumentList()
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.COMMA)),
              equals(Value()
                ..argumentList = (Value_ArgumentList()
                  ..id = 1
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.COMMA)));
        });
  
        test("with space separator", () async {
          expect(
              await _roundTrip(Value()
                ..argumentList = (Value_ArgumentList()
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.SPACE)),
              equals(Value()
                ..argumentList = (Value_ArgumentList()
                  ..id = 1
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.SPACE)));
        });
  
        test("with slash separator", () async {
          expect(
              await _roundTrip(Value()
                ..argumentList = (Value_ArgumentList()
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.SLASH)),
              equals(Value()
                ..argumentList = (Value_ArgumentList()
                  ..id = 1
                  ..contents.addAll([_true, _false, _null])
                  ..separator = ListSeparator.SLASH)));
        });
  
        test("with keywords", () async {
          expect(
              await _roundTrip(Value()
                ..argumentList = (Value_ArgumentList()
                  ..keywords.addAll({"arg1": _true, "arg2": _false})
                  ..separator = ListSeparator.COMMA)),
              equals(Value()
                ..argumentList = (Value_ArgumentList()
                  ..id = 1
                  ..keywords.addAll({"arg1": _true, "arg2": _false})
                  ..separator = ListSeparator.COMMA)));
        });
      });
  
      group("a map", () {
        group("with no elements", () {
          _testSerializationAndRoundTrip(Value()..map = Value_Map(), "()",
              inspect: true);
        });
  
        test("with one element", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..map = (Value_Map()
                      ..entries.add(Value_Map_Entry()
                        ..key = _true
                        ..value = _false)),
                  inspect: true),
              "(true: false)");
        });
  
        test("with multiple elements", () async {
          expect(
              await _deprotofy(
                  Value()
                    ..map = (Value_Map()
                      ..entries.addAll([
                        Value_Map_Entry()
                          ..key = _true
                          ..value = _false,
                        Value_Map_Entry()
                          ..key =
                              (Value()..number = (Value_Number()..value = 1.0))
                          ..value =
                              (Value()..number = (Value_Number()..value = 2.0)),
                        Value_Map_Entry()
                          ..key = (Value()
                            ..string = (Value_String()
                              ..text = "a"
                              ..quoted = false))
                          ..value = (Value()
                            ..string = (Value_String()
                              ..text = "b"
                              ..quoted = false))
                      ])),
                  inspect: true),
              "(true: false, 1: 2, a: b)");
        });
      });
  
      group("a calculation", () {
        test("with a string argument", () async {
          expect(
              await _deprotofy(Value()
                ..calculation = (Value_Calculation()
                  ..name = "calc"
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..string = "var(--foo)"))),
              "calc(var(--foo))");
        });
  
        test("with an interpolation argument", () async {
          expect(
              await _deprotofy(Value()
                ..calculation = (Value_Calculation()
                  ..name = "calc"
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..interpolation = "var(--foo)"))),
              "calc(var(--foo))");
        });
  
        test("with number arguments", () async {
          expect(
              await _deprotofy(Value()
                ..calculation = (Value_Calculation()
                  ..name = "clamp"
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..number = (Value_Number()
                      ..value = 1.0
                      ..numerators.add("%")))
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..number = (Value_Number()
                      ..value = 2.0
                      ..numerators.add("px")))
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..number = (Value_Number()
                      ..value = 3.0
                      ..numerators.add("em"))))),
              "clamp(1%, 2px, 3em)");
        });
  
        test("with a calculation argument", () async {
          expect(
              await _deprotofy(Value()
                ..calculation = (Value_Calculation()
                  ..name = "min"
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..calculation = (Value_Calculation()
                      ..name = "max"
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()
                          ..value = 1.0
                          ..numerators.add("%")))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()
                          ..value = 2.0
                          ..numerators.add("px")))))
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..number = (Value_Number()
                      ..value = 3.0
                      ..numerators.add("em"))))),
              "min(max(1%, 2px), 3em)");
        });
  
        test("with an operation", () async {
          expect(
              await _deprotofy(Value()
                ..calculation = (Value_Calculation()
                  ..name = "calc"
                  ..arguments.add(Value_Calculation_CalculationValue()
                    ..operation = (Value_Calculation_CalculationOperation()
                      ..operator = CalculationOperator.PLUS
                      ..left = (Value_Calculation_CalculationValue()
                        ..number = (Value_Number()
                          ..value = 1.0
                          ..numerators.add("%")))
                      ..right = (Value_Calculation_CalculationValue()
                        ..number = (Value_Number()
                          ..value = 2.0
                          ..numerators.add("px"))))))),
              "calc(1% + 2px)");
        });
  
        group("simplifies", () {
          test("an operation", () async {
            expect(
                await _deprotofy(Value()
                  ..calculation = (Value_Calculation()
                    ..name = "calc"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..operation = (Value_Calculation_CalculationOperation()
                        ..operator = CalculationOperator.PLUS
                        ..left = (Value_Calculation_CalculationValue()
                          ..number = (Value_Number()..value = 1.0))
                        ..right = (Value_Calculation_CalculationValue()
                          ..number = (Value_Number()..value = 2.0)))))),
                "3");
          });
  
          test("a nested operation", () async {
            expect(
                await _deprotofy(Value()
                  ..calculation = (Value_Calculation()
                    ..name = "calc"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..operation = (Value_Calculation_CalculationOperation()
                        ..operator = CalculationOperator.PLUS
                        ..left = (Value_Calculation_CalculationValue()
                          ..number = (Value_Number()
                            ..value = 1.0
                            ..numerators.add("%")))
                        ..right = (Value_Calculation_CalculationValue()
                          ..operation = (Value_Calculation_CalculationOperation()
                            ..operator = CalculationOperator.PLUS
                            ..left = (Value_Calculation_CalculationValue()
                              ..number = (Value_Number()
                                ..value = 2.0
                                ..numerators.add("px")))
                            ..right = (Value_Calculation_CalculationValue()
                              ..number = (Value_Number()
                                ..value = 3.0
                                ..numerators.add("px"))))))))),
                "calc(1% + 5px)");
          });
  
          test("min", () async {
            expect(
                await _deprotofy(Value()
                  ..calculation = (Value_Calculation()
                    ..name = "min"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 1.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 2.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 3.0)))),
                "1");
          });
  
          test("max", () async {
            expect(
                await _deprotofy(Value()
                  ..calculation = (Value_Calculation()
                    ..name = "max"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 1.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 2.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 3.0)))),
                "3");
          });
  
          test("clamp", () async {
            expect(
                await _deprotofy(Value()
                  ..calculation = (Value_Calculation()
                    ..name = "clamp"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 1.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 2.0))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()..value = 3.0)))),
                "2");
          });
        });
      });
  
      test("true", () async {
        expect(await _deprotofy(_true), equals("true"));
      });
  
      test("false", () async {
        expect(await _deprotofy(_false), equals("false"));
      });
  
      test("null", () async {
        expect(await _deprotofy(_null, inspect: true), equals("null"));
      });
  
      group("and rejects", () {
        group("a color", () {
          test("with red above 255", () async {
            await _expectDeprotofyError(_rgb(256, 0, 0, 1.0),
                "RgbColor.red must be between 0 and 255, was 256");
          });
  
          test("with green above 255", () async {
            await _expectDeprotofyError(_rgb(0, 256, 0, 1.0),
                "RgbColor.green must be between 0 and 255, was 256");
          });
  
          test("with blue above 255", () async {
            await _expectDeprotofyError(_rgb(0, 0, 256, 1.0),
                "RgbColor.blue must be between 0 and 255, was 256");
          });
  
          test("with RGB alpha below 0", () async {
            await _expectDeprotofyError(_rgb(0, 0, 0, -0.1),
                "RgbColor.alpha must be between 0 and 1, was -0.1");
          });
  
          test("with RGB alpha above 1", () async {
            await _expectDeprotofyError(_rgb(0, 0, 0, 1.1),
                "RgbColor.alpha must be between 0 and 1, was 1.1");
          });
  
          test("with saturation below 0", () async {
            await _expectDeprotofyError(_hsl(0, -0.1, 0, 1.0),
                "HslColor.saturation must be between 0 and 100, was -0.1");
          });
  
          test("with saturation above 100", () async {
            await _expectDeprotofyError(
                _hsl(0, 100.1, 0, 1.0),
                "HslColor.saturation must be between 0 and 100, was "
                "100.1");
          });
  
          test("with lightness below 0", () async {
            await _expectDeprotofyError(_hsl(0, 0, -0.1, 1.0),
                "HslColor.lightness must be between 0 and 100, was -0.1");
          });
  
          test("with lightness above 100", () async {
            await _expectDeprotofyError(
                _hsl(0, 0, 100.1, 1.0),
                "HslColor.lightness must be between 0 and 100, was "
                "100.1");
          });
  
          test("with HSL alpha below 0", () async {
            await _expectDeprotofyError(_hsl(0, 0, 0, -0.1),
                "HslColor.alpha must be between 0 and 1, was -0.1");
          });
  
          test("with HSL alpha above 1", () async {
            await _expectDeprotofyError(_hsl(0, 0, 0, 1.1),
                "HslColor.alpha must be between 0 and 1, was 1.1");
          });
        });
  
        test("a list with multiple elements and an unknown separator", () async {
          await _expectDeprotofyError(
              Value()
                ..list = (Value_List()
                  ..contents.addAll([_true, _false])
                  ..separator = ListSeparator.UNDECIDED),
              endsWith("can't have an undecided separator because it has 2 "
                  "elements"));
        });
  
        test("an arglist with an unknown id", () async {
          await _expectDeprotofyError(
              Value()..argumentList = (Value_ArgumentList()..id = 1),
              equals(
                  "Value.ArgumentList.id 1 doesn't match any known argument lists"));
        });
  
        group("a calculation", () {
          group("with too many arguments", () {
            test("for calc", () async {
              await _expectDeprotofyError(
                  Value()
                    ..calculation = (Value_Calculation()
                      ..name = "calc"
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 1.0))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 2.0))),
                  equals("Value.Calculation.arguments must have exactly one "
                      "argument for calc()."));
            });
  
            test("for clamp", () async {
              await _expectDeprotofyError(
                  Value()
                    ..calculation = (Value_Calculation()
                      ..name = "clamp"
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 1.0))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 2.0))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 3.0))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 4.0))),
                  equals("Value.Calculation.arguments must have exactly 3 "
                      "arguments for clamp()."));
            });
          });
  
          group("with too few arguments", () {
            test("for calc", () async {
              await _expectDeprotofyError(
                  Value()..calculation = (Value_Calculation()..name = "calc"),
                  equals("Value.Calculation.arguments must have exactly one "
                      "argument for calc()."));
            });
  
            test("for clamp", () async {
              await _expectDeprotofyError(
                  Value()
                    ..calculation = (Value_Calculation()
                      ..name = "clamp"
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 1.0))
                      ..arguments.add(Value_Calculation_CalculationValue()
                        ..number = (Value_Number()..value = 2.0))),
                  equals("Value.Calculation.arguments must have exactly 3 "
                      "arguments for clamp()."));
            });
  
            test("for min", () async {
              await _expectDeprotofyError(
                  Value()..calculation = (Value_Calculation()..name = "min"),
                  equals("Value.Calculation.arguments must have at least 1 "
                      "argument for min()."));
            });
  
            test("for max", () async {
              await _expectDeprotofyError(
                  Value()..calculation = (Value_Calculation()..name = "max"),
                  equals("Value.Calculation.arguments must have at least 1 "
                      "argument for max()."));
            });
          });
  
          test("reports a compilation failure when simplification fails",
              () async {
            _process.inbound
                .add(compileString("a {b: foo()}", functions: [r"foo()"]));
  
            var request = getFunctionCallRequest(await _process.outbound.next);
            expect(request.arguments, isEmpty);
            _process.inbound.add(InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..success = (Value()
                  ..calculation = (Value_Calculation()
                    ..name = "min"
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 1.0
                        ..numerators.add("px")))
                    ..arguments.add(Value_Calculation_CalculationValue()
                      ..number = (Value_Number()
                        ..value = 2.0
                        ..numerators.add("s")))))));
  
            var failure = getCompileFailure(await _process.outbound.next);
            expect(failure.message, equals("1px and 2s are incompatible."));
            expect(_process.kill(), completes);
          });
        });
  
        group("reports a compilation error for a function with a signature", () {
          Future<void> expectSignatureError(
              String signature, Object message) async {
            _process.inbound.add(
                compileString("a {b: inspect(foo())}", functions: [r"foo()"]));
  
            var request = getFunctionCallRequest(await _process.outbound.next);
            expect(request.arguments, isEmpty);
            _process.inbound.add(InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..success = (Value()
                  ..hostFunction = (Value_HostFunction()
                    ..id = 1234
                    ..signature = signature))));
  
            var failure = getCompileFailure(await _process.outbound.next);
            expect(failure.message, message);
            expect(_process.kill(), completes);
          }
  
          test("that's empty", () async {
            await expectSignatureError(
                "", r'Invalid signature "": Expected identifier.');
          });
  
          test("that's just a name", () async {
            await expectSignatureError(
                "foo", r'Invalid signature "foo": expected "(".');
          });
  
          test("without a closing paren", () async {
            await expectSignatureError(
                r"foo($bar", r'Invalid signature "foo($bar": expected ")".');
          });
  
          test("with text after the closing paren", () async {
            await expectSignatureError(r"foo() ",
                r'Invalid signature "foo() ": expected no more input.');
          });
  
          test("with invalid arguments", () async {
            await expectSignatureError(
                r"foo($)", r'Invalid signature "foo($)": Expected identifier.');
          });
        });
      });
    });
  }
  
  /// Evaluates [sassScript] in the compiler, passes it to a custom function, and
  /// returns the protocol buffer result.
  Future<Value> _protofy(String sassScript) async {
    _process.inbound.add(compileString("""
  @use 'sass:list';
  @use 'sass:map';
  @use 'sass:math';
  @use 'sass:meta';
  
  @function capture-args(\$args...) {
    \$_: meta.keywords(\$args);
    @return \$args;
  }
  
  \$_: foo(($sassScript));
  """, functions: [r"foo($arg)"]));
    var request = getFunctionCallRequest(await _process.outbound.next);
    expect(_process.kill(), completes);
    return request.arguments.single;
  }
  
  /// Defines two tests: one that asserts that [value] is serialized to the CSS
  /// value [expected], and one that asserts that it survives a round trip in the
  /// same protocol buffer format.
  ///
  /// This is necessary for values that can be serialized but also have metadata
  /// that's not visible in the serialized form.
  void _testSerializationAndRoundTrip(Value value, String expected,
      {bool inspect = false}) {
    test("is serialized correctly",
        () async => expect(await _deprotofy(value, inspect: inspect), expected));
  
    test("preserves metadata", () => _assertRoundTrips(value));
  }
  
  /// Sends [value] to the compiler and returns its string serialization.
  ///
  /// If [inspect] is true, this returns the value as serialized by the
  /// `meta.inspect()` function.
  Future<String> _deprotofy(Value value, {bool inspect = false}) async {
    _process.inbound.add(compileString(
        inspect ? "a {b: inspect(foo())}" : "a {b: foo()}",
        functions: [r"foo()"]));
  
    var request = getFunctionCallRequest(await _process.outbound.next);
    expect(request.arguments, isEmpty);
    _process.inbound.add(InboundMessage()
      ..functionCallResponse = (InboundMessage_FunctionCallResponse()
        ..id = request.id
        ..success = value));
  
    var success = getCompileSuccess(await _process.outbound.next);
    expect(_process.kill(), completes);
    return RegExp(r"  b: (.*);").firstMatch(success.css)![1]!;
  }
  
  /// Asserts that [value] causes a parameter error with a message matching
  /// [message] when deserializing it from a protocol buffer.
  Future<void> _expectDeprotofyError(Value value, Object message) async {
    _process.inbound.add(compileString("a {b: foo()}", functions: [r"foo()"]));
  
    var request = getFunctionCallRequest(await _process.outbound.next);
    expect(request.arguments, isEmpty);
    _process.inbound.add(InboundMessage()
      ..functionCallResponse = (InboundMessage_FunctionCallResponse()
        ..id = request.id
        ..success = value));
  
    await expectParamsError(_process, errorId, message);
    await _process.kill();
  }
  
  /// Sends [value] to the compiler to convert to a native Sass value, then sends
  /// it back out to the host as a protocol buffer and asserts the two buffers are
  /// identical.
  ///
  /// Generally [_deprotofy] should be used instead unless there are details about
  /// the internal structure of the value that won't show up in its string
  /// representation.
  Future<void> _assertRoundTrips(Value value) async =>
      expect(await _roundTrip(value), equals(value));
  
  /// Sends [value] to the compiler to convert to a native Sass value, then sends
  /// it back out to the host as a protocol buffer and returns the result.
  Future<Value> _roundTrip(Value value) async {
    _process.inbound.add(compileString("""
  \$_: outbound(inbound());
  """, functions: ["inbound()", r"outbound($arg)"]));
  
    var request = getFunctionCallRequest(await _process.outbound.next);
    expect(request.arguments, isEmpty);
    _process.inbound.add(InboundMessage()
      ..functionCallResponse = (InboundMessage_FunctionCallResponse()
        ..id = request.id
        ..success = value));
  
    request = getFunctionCallRequest(await _process.outbound.next);
    expect(_process.kill(), completes);
    return request.arguments.single;
  }
  
  /// Returns a [Value] that's an RGB color with the given fields.
  Value _rgb(int red, int green, int blue, double alpha) => Value()
    ..rgbColor = (Value_RgbColor()
      ..red = red
      ..green = green
      ..blue = blue
      ..alpha = alpha);
  
  /// Returns a [Value] that's an HSL color with the given fields.
  Value _hsl(num hue, num saturation, num lightness, double alpha) => Value()
    ..hslColor = (Value_HslColor()
      ..hue = hue * 1.0
      ..saturation = saturation * 1.0
      ..lightness = lightness * 1.0
      ..alpha = alpha);
  
  /// Asserts that [process] emits a [CompileFailure] result with the given
  /// [message] on its protobuf stream and causes the compilation to fail.
  Future<void> _expectFunctionError(
      EmbeddedProcess process, Object message) async {
    var failure = getCompileFailure(await process.outbound.next);
    expect(failure.message, equals(message));
  }
  