// 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.

@TestOn('vm')
library;

import 'package:test/test.dart';

import 'package:sass/src/embedded/embedded_sass.pb.dart';
import 'package:sass/src/embedded/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.send(compileString("a {b: c}", functions: [r""]));
      await _expectFunctionError(
        _process,
        r'Invalid signature "": Expected identifier.',
      );
      await _process.close();
    });

    test("that's just a name", () async {
      _process.send(compileString("a {b: c}", functions: [r"foo"]));
      await _expectFunctionError(
        _process,
        r'Invalid signature "foo": expected "(".',
      );
      await _process.close();
    });

    test("without a closing paren", () async {
      _process.send(compileString("a {b: c}", functions: [r"foo($bar"]));
      await _expectFunctionError(
        _process,
        r'Invalid signature "foo($bar": expected ")".',
      );
      await _process.close();
    });

    test("with text after the closing paren", () async {
      _process.send(compileString("a {b: c}", functions: [r"foo() "]));
      await _expectFunctionError(
        _process,
        r'Invalid signature "foo() ": expected no more input.',
      );
      await _process.close();
    });

    test("with invalid arguments", () async {
      _process.send(compileString("a {b: c}", functions: [r"foo($)"]));
      await _expectFunctionError(
        _process,
        r'Invalid signature "foo($)": Expected identifier.',
      );
      await _process.close();
    });
  });

  group("includes in FunctionCallRequest", () {
    test("the function name", () async {
      _process.send(compileString("a {b: foo()}", functions: ["foo()"]));
      var request = await getFunctionCallRequest(_process);
      expect(request.name, equals("foo"));
      await _process.kill();
    });

    group("arguments", () {
      test("that are empty", () async {
        _process.send(compileString("a {b: foo()}", functions: ["foo()"]));
        var request = await getFunctionCallRequest(_process);
        expect(request.arguments, isEmpty);
        await _process.kill();
      });

      test("by position", () async {
        _process.send(
          compileString(
            "a {b: foo(true, null, false)}",
            functions: [r"foo($arg1, $arg2, $arg3)"],
          ),
        );
        var request = await getFunctionCallRequest(_process);
        expect(request.arguments, equals([_true, _null, _false]));
        await _process.kill();
      });

      test("by name", () async {
        _process.send(
          compileString(
            r"a {b: foo($arg3: true, $arg1: null, $arg2: false)}",
            functions: [r"foo($arg1, $arg2, $arg3)"],
          ),
        );
        var request = await getFunctionCallRequest(_process);
        expect(request.arguments, equals([_null, _false, _true]));
        await _process.kill();
      });

      test("by position and name", () async {
        _process.send(
          compileString(
            r"a {b: foo(true, $arg3: null, $arg2: false)}",
            functions: [r"foo($arg1, $arg2, $arg3)"],
          ),
        );
        var request = await getFunctionCallRequest(_process);
        expect(request.arguments, equals([_true, _false, _null]));
        await _process.kill();
      });

      test("from defaults", () async {
        _process.send(
          compileString(
            r"a {b: foo(1, $arg3: 2)}",
            functions: [r"foo($arg1: null, $arg2: true, $arg3: false)"],
          ),
        );
        var request = await getFunctionCallRequest(_process);
        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.send(
            compileString(
              "a {b: foo(true, false, null)}",
              functions: [r"foo($arg, $args...)"],
            ),
          );
          var request = await getFunctionCallRequest(_process);

          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.send(
            compileString(
              r"a {b: foo(true, $arg: false)}",
              functions: [r"foo($args...)"],
            ),
          );
          var request = await getFunctionCallRequest(_process);

          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.send(
            compileString(
              r"a {b: foo($arg: false)}",
              functions: [r"foo($args...)"],
            ),
          );
          var request = await getFunctionCallRequest(_process);

          _process.send(
            InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..success = _true),
          );

          var failure = await getCompileFailure(_process);
          expect(failure.message, equals(r"No parameter named $arg."));
          await _process.close();
        });

        test("doesn't throw if named arguments are used", () async {
          _process.send(
            compileString(
              r"a {b: foo($arg: false)}",
              functions: [r"foo($args...)"],
            ),
          );
          var request = await getFunctionCallRequest(_process);

          _process.send(
            InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..accessedArgumentLists.add(
                  request.arguments.first.argumentList.id,
                )
                ..success = _true),
          );

          await expectSuccess(_process, equals("a {\n  b: true;\n}"));
          await _process.close();
        });
      });
    });
  });

  test("returns the result as a SassScript value", () async {
    _process.send(compileString("a {b: foo() + 2px}", functions: [r"foo()"]));
    var request = await getFunctionCallRequest(_process);

    _process.send(
      InboundMessage()
        ..functionCallResponse = (InboundMessage_FunctionCallResponse()
          ..id = request.id
          ..success = (Value()
            ..number = (Value_Number()
              ..value = 1
              ..numerators.add("px")))),
    );

    await expectSuccess(_process, equals("a {\n  b: 3px;\n}"));
    await _process.close();
  });

  group("calls a first-class function", () {
    test("defined in the compiler and passed to and from the host", () async {
      _process.send(
        compileString(
          r"""
            @use "sass:math";
            @use "sass:meta";

            a {b: meta.call(foo(meta.get-function("abs", $module: "math")), -1)}
          """,
          functions: [r"foo($arg)"],
        ),
      );

      var request = await getFunctionCallRequest(_process);
      var value = request.arguments.single;
      expect(value.hasCompilerFunction(), isTrue);
      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = value),
      );

      await expectSuccess(_process, equals("a {\n  b: 1;\n}"));
      await _process.close();
    });

    test("defined in the host", () async {
      _process.send(
        compileString(
          """
            @use "sass:meta";

            a {b: meta.call(foo(), true)}
          """,
          functions: [r"foo()"],
        ),
      );

      var hostFunctionId = 5678;
      var request = await getFunctionCallRequest(_process);
      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = (Value()
              ..hostFunction = (Value_HostFunction()
                ..id = hostFunctionId
                ..signature = r"bar($arg)"))),
      );

      request = await getFunctionCallRequest(_process);
      expect(request.functionId, equals(hostFunctionId));
      expect(request.arguments, equals([_true]));

      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = _false),
      );

      await expectSuccess(_process, equals("a {\n  b: false;\n}"));
      await _process.close();
    });

    test("defined in the host and passed to and from the host", () async {
      _process.send(
        compileString(
          r"""
            @use "sass:meta";

            $function: get-host-function();
            $function: round-trip($function);
            a {b: meta.call($function, true)}
          """,
          functions: [r"get-host-function()", r"round-trip($function)"],
        ),
      );

      var hostFunctionId = 5678;
      var request = await getFunctionCallRequest(_process);
      expect(request.name, equals("get-host-function"));
      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = (Value()
              ..hostFunction = (Value_HostFunction()
                ..id = hostFunctionId
                ..signature = r"bar($arg)"))),
      );

      request = await getFunctionCallRequest(_process);
      expect(request.name, equals("round-trip"));
      var value = request.arguments.single;
      expect(value.hasCompilerFunction(), isTrue);
      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = value),
      );

      request = await getFunctionCallRequest(_process);
      expect(request.functionId, equals(hostFunctionId));
      expect(request.arguments, equals([_true]));

      _process.send(
        InboundMessage()
          ..functionCallResponse = (InboundMessage_FunctionCallResponse()
            ..id = request.id
            ..success = _false),
      );

      await expectSuccess(_process, equals("a {\n  b: false;\n}"));
      await _process.close();
    });
  });

  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('string.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()..string = "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,
          ),
          "calc(1em * 1px * 1foo)",
        );
      });

      test("with one denominator", () async {
        expect(
          await _deprotofy(
            Value()
              ..number = (Value_Number()
                ..value = 1
                ..denominators.add("em")),
            inspect: true,
          ),
          "calc(1 / 1em)",
        );
      });

      test("with multiple denominators", () async {
        expect(
          await _deprotofy(
            Value()
              ..number = (Value_Number()
                ..value = 1
                ..denominators.addAll(["em", "px", "foo"])),
            inspect: true,
          ),
          "calc(1 / 1em / 1px / 1foo)",
        );
      });

      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,
          ),
          "calc(1em * 1px / 1s / 1foo)",
        );
      });
    });

    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'),
            );
          });

          test("with red above 255", () async {
            expect(
              await _deprotofy(_rgb(256, 0, 0, 1.0)),
              equals('hsl(0, 100.7874015748%, 50.1960784314%)'),
            );
          });

          test("with green above 255", () async {
            expect(
              await _deprotofy(_rgb(0, 256, 0, 1.0)),
              equals('hsl(120, 100.7874015748%, 50.1960784314%)'),
            );
          });

          test("with blue above 255", () async {
            expect(
              await _deprotofy(_rgb(0, 0, 256, 1.0)),
              equals('hsl(240, 100.7874015748%, 50.1960784314%)'),
            );
          });
        });

        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)),
                "hsl(0, 50%, 50%)",
              );
            });

            test("360", () async {
              expect(
                await _deprotofy(_hsl(360, 50, 50, 1.0)),
                "hsl(0, 50%, 50%)",
              );
            });

            test("below 0", () async {
              expect(
                await _deprotofy(_hsl(-100, 50, 50, 1.0)),
                "hsl(260, 50%, 50%)",
              );
            });

            test("between 0 and 360", () async {
              expect(
                await _deprotofy(_hsl(100, 50, 50, 1.0)),
                "hsl(100, 50%, 50%)",
              );
            });

            test("above 360", () async {
              expect(
                await _deprotofy(_hsl(560, 50, 50, 1.0)),
                "hsl(200, 50%, 50%)",
              );
            });
          });

          group("saturation", () {
            test("0", () async {
              expect(await _deprotofy(_hsl(0, 0, 50, 1.0)), "hsl(0, 0%, 50%)");
            });

            test("100", () async {
              expect(
                await _deprotofy(_hsl(0, 100, 50, 1.0)),
                "hsl(0, 100%, 50%)",
              );
            });

            test("in the middle", () async {
              expect(
                await _deprotofy(_hsl(0, 42, 50, 1.0)),
                "hsl(0, 42%, 50%)",
              );
            });
          });

          group("lightness", () {
            test("0", () async {
              expect(await _deprotofy(_hsl(0, 50, 0, 1.0)), "hsl(0, 50%, 0%)");
            });

            test("100", () async {
              expect(
                await _deprotofy(_hsl(0, 50, 100, 1.0)),
                "hsl(0, 50%, 100%)",
              );
            });

            test("in the middle", () async {
              expect(
                await _deprotofy(_hsl(0, 50, 42, 1.0)),
                "hsl(0, 50%, 42%)",
              );
            });
          });
        });

        group("with alpha", () {
          test("0", () async {
            expect(
              await _deprotofy(_hsl(10, 20, 30, 0.0)),
              "hsla(10, 20%, 30%, 0)",
            );
          });

          test("between 0 and 1", () async {
            expect(
              await _deprotofy(_hsl(10, 20, 30, 0.123)),
              "hsla(10, 20%, 30%, 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 RGB alpha below 0", () async {
          await _expectDeprotofyError(
            _rgb(0, 0, 0, -0.1),
            "Color.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),
            "Color.alpha must be between 0 and 1, was 1.1",
          );
        });

        test("with HSL alpha below 0", () async {
          await _expectDeprotofyError(
            _hsl(0, 0, 0, -0.1),
            "Color.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),
            "Color.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 1 to 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"),
              equals(
                "Value.Calculation.arguments must have 1 to 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.send(compileString("a {b: foo()}", functions: [r"foo()"]));

            var request = await getFunctionCallRequest(_process);
            expect(request.arguments, isEmpty);
            _process.send(
              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 = await getCompileFailure(_process);
            expect(failure.message, equals("1px and 2s are incompatible."));
            expect(_process.close(), completes);
          },
        );
      });

      group("reports a compilation error for a function with a signature", () {
        Future<void> expectSignatureError(
          String signature,
          Object message,
        ) async {
          _process.send(
            compileString(
              "@use 'sass:meta';\na {b: meta.inspect(foo())}",
              functions: [r"foo()"],
            ),
          );

          var request = await getFunctionCallRequest(_process);
          expect(request.arguments, isEmpty);
          _process.send(
            InboundMessage()
              ..functionCallResponse = (InboundMessage_FunctionCallResponse()
                ..id = request.id
                ..success = (Value()
                  ..hostFunction = (Value_HostFunction()
                    ..id = 1234
                    ..signature = signature))),
          );

          var failure = await getCompileFailure(_process);
          expect(failure.message, message);
          expect(_process.close(), 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.send(
    compileString(
      """
        @use 'sass:list';
        @use 'sass:map';
        @use 'sass:math';
        @use 'sass:meta';
        @use 'sass:string';

        @function capture-args(\$args...) {
          \$_: meta.keywords(\$args);
          @return \$args;
        }

        \$_: foo(($sassScript));
      """,
      functions: [r"foo($arg)"],
    ),
  );
  var request = await getFunctionCallRequest(_process);
  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.send(
    compileString(
      inspect
          ? "@use 'sass:meta';\na {b: meta.inspect(foo())}"
          : "a {b: foo()}",
      functions: [r"foo()"],
    ),
  );

  var request = await getFunctionCallRequest(_process);
  expect(request.arguments, isEmpty);
  _process.send(
    InboundMessage()
      ..functionCallResponse = (InboundMessage_FunctionCallResponse()
        ..id = request.id
        ..success = value),
  );

  var success = await getCompileSuccess(_process);
  expect(_process.close(), 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.send(compileString("a {b: foo()}", functions: [r"foo()"]));

  var request = await getFunctionCallRequest(_process);
  expect(request.arguments, isEmpty);
  _process.send(
    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.send(
    compileString(
      "\$_: outbound(inbound());",
      functions: ["inbound()", r"outbound($arg)"],
    ),
  );

  var request = await getFunctionCallRequest(_process);
  expect(request.arguments, isEmpty);
  _process.send(
    InboundMessage()
      ..functionCallResponse = (InboundMessage_FunctionCallResponse()
        ..id = request.id
        ..success = value),
  );

  request = await getFunctionCallRequest(_process);
  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()
  ..color = (Value_Color()
    ..space = 'rgb'
    ..channel1 = red * 1.0
    ..channel2 = green * 1.0
    ..channel3 = blue * 1.0
    ..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()
  ..color = (Value_Color()
    ..space = 'hsl'
    ..channel1 = hue * 1.0
    ..channel2 = saturation * 1.0
    ..channel3 = 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 = await getCompileFailure(process);
  expect(failure.message, equals(message));
}
